266 lines
9.6 KiB
Python
266 lines
9.6 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Calejo Control Adapter - Python SSH Deployment Script
|
||
|
|
Alternative deployment script using Python for more complex deployments
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import yaml
|
||
|
|
import paramiko
|
||
|
|
import argparse
|
||
|
|
import tempfile
|
||
|
|
import tarfile
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Dict, Any
|
||
|
|
|
||
|
|
|
||
|
|
class SSHDeployer:
|
||
|
|
"""SSH-based deployment manager"""
|
||
|
|
|
||
|
|
def __init__(self, config_file: str):
|
||
|
|
self.config_file = config_file
|
||
|
|
self.config = self.load_config()
|
||
|
|
self.ssh_client = None
|
||
|
|
self.sftp_client = None
|
||
|
|
|
||
|
|
def load_config(self) -> Dict[str, Any]:
|
||
|
|
"""Load deployment configuration from YAML file"""
|
||
|
|
try:
|
||
|
|
with open(self.config_file, 'r') as f:
|
||
|
|
config = yaml.safe_load(f)
|
||
|
|
|
||
|
|
# Validate required configuration
|
||
|
|
required = ['ssh.host', 'ssh.username', 'ssh.key_file']
|
||
|
|
for req in required:
|
||
|
|
keys = req.split('.')
|
||
|
|
current = config
|
||
|
|
for key in keys:
|
||
|
|
if key not in current:
|
||
|
|
raise ValueError(f"Missing required configuration: {req}")
|
||
|
|
current = current[key]
|
||
|
|
|
||
|
|
return config
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"❌ Error loading configuration: {e}")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
def connect(self) -> bool:
|
||
|
|
"""Establish SSH connection"""
|
||
|
|
try:
|
||
|
|
ssh_config = self.config['ssh']
|
||
|
|
|
||
|
|
# Create SSH client
|
||
|
|
self.ssh_client = paramiko.SSHClient()
|
||
|
|
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||
|
|
|
||
|
|
# Load private key
|
||
|
|
key_path = ssh_config['key_file']
|
||
|
|
if not os.path.exists(key_path):
|
||
|
|
print(f"❌ SSH key file not found: {key_path}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
private_key = paramiko.Ed25519Key.from_private_key_file(key_path)
|
||
|
|
|
||
|
|
# Connect
|
||
|
|
port = ssh_config.get('port', 22)
|
||
|
|
self.ssh_client.connect(
|
||
|
|
hostname=ssh_config['host'],
|
||
|
|
port=port,
|
||
|
|
username=ssh_config['username'],
|
||
|
|
pkey=private_key,
|
||
|
|
timeout=30
|
||
|
|
)
|
||
|
|
|
||
|
|
# Create SFTP client
|
||
|
|
self.sftp_client = self.ssh_client.open_sftp()
|
||
|
|
|
||
|
|
print(f"✅ Connected to {ssh_config['host']}")
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"❌ SSH connection failed: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def execute_remote(self, command: str, description: str = "") -> bool:
|
||
|
|
"""Execute command on remote server"""
|
||
|
|
try:
|
||
|
|
if description:
|
||
|
|
print(f"🔧 {description}")
|
||
|
|
|
||
|
|
stdin, stdout, stderr = self.ssh_client.exec_command(command)
|
||
|
|
exit_status = stdout.channel.recv_exit_status()
|
||
|
|
|
||
|
|
if exit_status == 0:
|
||
|
|
if description:
|
||
|
|
print(f" ✅ {description} completed")
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
error_output = stderr.read().decode()
|
||
|
|
print(f" ❌ {description} failed: {error_output}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f" ❌ {description} failed: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def transfer_file(self, local_path: str, remote_path: str, description: str = "") -> bool:
|
||
|
|
"""Transfer file to remote server"""
|
||
|
|
try:
|
||
|
|
if description:
|
||
|
|
print(f"📁 {description}")
|
||
|
|
|
||
|
|
self.sftp_client.put(local_path, remote_path)
|
||
|
|
|
||
|
|
if description:
|
||
|
|
print(f" ✅ {description} completed")
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f" ❌ {description} failed: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def create_deployment_package(self) -> str:
|
||
|
|
"""Create deployment package excluding sensitive files"""
|
||
|
|
temp_dir = tempfile.mkdtemp()
|
||
|
|
package_path = os.path.join(temp_dir, "deployment.tar.gz")
|
||
|
|
|
||
|
|
# Create tar.gz package
|
||
|
|
with tarfile.open(package_path, "w:gz") as tar:
|
||
|
|
# Add all files except deployment config and keys
|
||
|
|
for root, dirs, files in os.walk('.'):
|
||
|
|
# Skip deployment directories
|
||
|
|
if 'deploy/config' in root or 'deploy/keys' in root:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Skip hidden directories
|
||
|
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||
|
|
|
||
|
|
for file in files:
|
||
|
|
if not file.startswith('.'):
|
||
|
|
file_path = os.path.join(root, file)
|
||
|
|
arcname = os.path.relpath(file_path, '.')
|
||
|
|
tar.add(file_path, arcname=arcname)
|
||
|
|
|
||
|
|
return package_path
|
||
|
|
|
||
|
|
def deploy(self, dry_run: bool = False):
|
||
|
|
"""Main deployment process"""
|
||
|
|
print("🚀 Starting SSH deployment...")
|
||
|
|
|
||
|
|
if dry_run:
|
||
|
|
print("🔍 DRY RUN MODE - No changes will be made")
|
||
|
|
|
||
|
|
# Connect to server
|
||
|
|
if not self.connect():
|
||
|
|
return False
|
||
|
|
|
||
|
|
try:
|
||
|
|
deployment_config = self.config['deployment']
|
||
|
|
target_dir = deployment_config['target_dir']
|
||
|
|
|
||
|
|
# Check prerequisites
|
||
|
|
print("🔍 Checking prerequisites...")
|
||
|
|
if not self.execute_remote("command -v docker", "Checking Docker"):
|
||
|
|
return False
|
||
|
|
if not self.execute_remote("command -v docker-compose", "Checking Docker Compose"):
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Create directories
|
||
|
|
print("📁 Creating directories...")
|
||
|
|
dirs = [
|
||
|
|
target_dir,
|
||
|
|
deployment_config.get('backup_dir', '/var/backup/calejo'),
|
||
|
|
deployment_config.get('log_dir', '/var/log/calejo'),
|
||
|
|
deployment_config.get('config_dir', '/etc/calejo')
|
||
|
|
]
|
||
|
|
|
||
|
|
for dir_path in dirs:
|
||
|
|
cmd = f"sudo mkdir -p {dir_path} && sudo chown {self.config['ssh']['username']}:{self.config['ssh']['username']} {dir_path}"
|
||
|
|
if not self.execute_remote(cmd, f"Creating {dir_path}"):
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Create deployment package
|
||
|
|
print("📦 Creating deployment package...")
|
||
|
|
package_path = self.create_deployment_package()
|
||
|
|
|
||
|
|
if dry_run:
|
||
|
|
print(f" 📦 Would transfer package: {package_path}")
|
||
|
|
os.remove(package_path)
|
||
|
|
return True
|
||
|
|
|
||
|
|
# Transfer package
|
||
|
|
remote_package_path = os.path.join(target_dir, "deployment.tar.gz")
|
||
|
|
if not self.transfer_file(package_path, remote_package_path, "Transferring deployment package"):
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Extract package
|
||
|
|
if not self.execute_remote(f"cd {target_dir} && tar -xzf deployment.tar.gz && rm deployment.tar.gz", "Extracting package"):
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Set permissions
|
||
|
|
if not self.execute_remote(f"chmod +x {target_dir}/scripts/*.sh", "Setting script permissions"):
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Build and start services
|
||
|
|
print("🐳 Building and starting services...")
|
||
|
|
if not self.execute_remote(f"cd {target_dir} && sudo docker-compose build", "Building Docker images"):
|
||
|
|
return False
|
||
|
|
if not self.execute_remote(f"cd {target_dir} && sudo docker-compose up -d", "Starting services"):
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Wait for services
|
||
|
|
print("⏳ Waiting for services to start...")
|
||
|
|
for i in range(30):
|
||
|
|
if self.execute_remote("curl -s http://localhost:8080/health > /dev/null", "", silent=True):
|
||
|
|
print(" ✅ Services started successfully")
|
||
|
|
break
|
||
|
|
print(f" ⏳ Waiting... ({i+1}/30)")
|
||
|
|
import time
|
||
|
|
time.sleep(2)
|
||
|
|
else:
|
||
|
|
print(" ❌ Services failed to start within 60 seconds")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Validate deployment
|
||
|
|
print("🔍 Validating deployment...")
|
||
|
|
self.execute_remote(f"cd {target_dir} && ./validate-deployment.sh", "Running validation")
|
||
|
|
|
||
|
|
print("🎉 Deployment completed successfully!")
|
||
|
|
return True
|
||
|
|
|
||
|
|
finally:
|
||
|
|
# Cleanup
|
||
|
|
if hasattr(self, 'package_path') and os.path.exists(self.package_path):
|
||
|
|
os.remove(self.package_path)
|
||
|
|
|
||
|
|
# Close connections
|
||
|
|
if self.sftp_client:
|
||
|
|
self.sftp_client.close()
|
||
|
|
if self.ssh_client:
|
||
|
|
self.ssh_client.close()
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""Main function"""
|
||
|
|
parser = argparse.ArgumentParser(description='Calejo Control Adapter - SSH Deployment')
|
||
|
|
parser.add_argument('-c', '--config', required=True, help='Deployment configuration file')
|
||
|
|
parser.add_argument('--dry-run', action='store_true', help='Dry run mode')
|
||
|
|
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
# Check if config file exists
|
||
|
|
if not os.path.exists(args.config):
|
||
|
|
print(f"❌ Configuration file not found: {args.config}")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
# Run deployment
|
||
|
|
deployer = SSHDeployer(args.config)
|
||
|
|
success = deployer.deploy(dry_run=args.dry_run)
|
||
|
|
|
||
|
|
sys.exit(0 if success else 1)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|