Guest User

Untitled

a guest
Jul 30th, 2025
6
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.07 KB | None | 0 0
  1.  
  2. #!/usr/bin/env python3
  3. import os
  4. import sys
  5. import json
  6. import yaml
  7. import time
  8. import hashlib
  9. import requests
  10. import tempfile
  11. import subprocess
  12. import argparse
  13. import threading
  14. import socket
  15. from pathlib import Path
  16. from urllib.parse import urlparse
  17.  
  18. class AIVMManager:
  19.     def __init__(self, config):
  20.         self.config = config
  21.         self.work_dir = Path(config.get('work_dir', './vm_instances'))
  22.         self.work_dir.mkdir(exist_ok=True)
  23.  
  24.         self.instance_name = config.get('instance_name', 'ai-agent')
  25.         self.vm_memory = config.get('memory', '2G')
  26.         self.vm_cpus = config.get('cpus', 2)
  27.         self.vm_disk_size = config.get('disk_size', '20G')
  28.  
  29.         self.ssh_port = config.get('ssh_port', 2222)
  30.         self.ssh_user = config.get('ssh_user', 'aiagent')
  31.         self.ssh_password = config.get('ssh_password', 'aiagent123')
  32.  
  33.         self.instance_dir = self.work_dir / self.instance_name
  34.         self.instance_dir.mkdir(exist_ok=True)
  35.         self.pid_file = self.instance_dir / 'qemu.pid'
  36.         self.serial_socket = self.instance_dir / 'serial.sock'
  37.         self.ssh_ready_flag = self.instance_dir / 'ssh_ready'
  38.  
  39.         self.images_dir = self.work_dir / 'images'
  40.         self.images_dir.mkdir(exist_ok=True)
  41.  
  42.     def log(self, message, level="INFO"):
  43.         timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
  44.         print(f"[{timestamp}] {level}: {message}")
  45.  
  46.         log_file = self.instance_dir / 'deploy.log'
  47.         with open(log_file, 'a') as f:
  48.             f.write(f"[{timestamp}] {level}: {message}\n")
  49.  
  50.     def check_dependencies(self):
  51.         required_tools = ['qemu-system-x86_64', 'qemu-img', 'genisoimage', 'sshpass']
  52.         missing_tools = []
  53.  
  54.         for tool in required_tools:
  55.             if not self._command_exists(tool):
  56.                 missing_tools.append(tool)
  57.  
  58.         if missing_tools:
  59.             self.log(f"Missing required tools: {', '.join(missing_tools)}", "ERROR")
  60.             self.log("Install with: apt-get install qemu-system-x86 qemu-utils genisoimage sshpass")
  61.             return False
  62.  
  63.         try:
  64.             self.work_dir.mkdir(exist_ok=True)
  65.             self.images_dir.mkdir(exist_ok=True)
  66.             self.instance_dir.mkdir(exist_ok=True)
  67.  
  68.             test_file = self.work_dir / 'test_write'
  69.             test_file.write_text('test')
  70.             test_file.unlink()
  71.  
  72.             self.log(f"Work directory: {self.work_dir.resolve()}")
  73.             self.log(f"Images directory: {self.images_dir.resolve()}")
  74.             self.log(f"Instance directory: {self.instance_dir.resolve()}")
  75.  
  76.         except Exception as e:
  77.             self.log(f"Directory setup failed: {e}", "ERROR")
  78.             return False
  79.  
  80.         return True
  81.  
  82.     def _command_exists(self, command):
  83.         return subprocess.run(['which', command],
  84.                             capture_output=True, text=True).returncode == 0
  85.  
  86.     def is_running(self):
  87.         if not self.pid_file.exists():
  88.             return False
  89.  
  90.         try:
  91.             with open(self.pid_file, 'r') as f:
  92.                 pid = int(f.read().strip())
  93.  
  94.             os.kill(pid, 0)
  95.             return True
  96.         except (ProcessLookupError, ValueError):
  97.             if self.pid_file.exists():
  98.                 self.pid_file.unlink()
  99.             return False
  100.  
  101.     def download_base_image(self):
  102.         image_url = 'https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2'
  103.         image_path = self.images_dir / 'debian-12-base.qcow2'
  104.  
  105.         if image_path.exists():
  106.             self.log(f"Using cached base image: {image_path.resolve()}")
  107.             return image_path
  108.  
  109.         self.log(f"Downloading base image from {image_url}")
  110.         self.log(f"Will save to: {image_path.resolve()}")
  111.  
  112.         try:
  113.             response = requests.get(image_url, stream=True)
  114.             response.raise_for_status()
  115.  
  116.             total_size = int(response.headers.get('content-length', 0))
  117.             downloaded = 0
  118.  
  119.             image_path.parent.mkdir(parents=True, exist_ok=True)
  120.  
  121.             with open(image_path, 'wb') as f:
  122.                 for chunk in response.iter_content(chunk_size=8192):
  123.                     if chunk:
  124.                         f.write(chunk)
  125.                         downloaded += len(chunk)
  126.                         if total_size > 0:
  127.                             progress = (downloaded / total_size) * 100
  128.                             print(f"\rDownload progress: {progress:.1f}%", end='', flush=True)
  129.  
  130.             print()
  131.             self.log(f"Base image downloaded successfully to {image_path.resolve()}")
  132.  
  133.             if not image_path.exists():
  134.                 raise Exception("Downloaded file does not exist")
  135.  
  136.             file_size = image_path.stat().st_size
  137.             if file_size < 100 * 1024 * 1024:
  138.                 raise Exception(f"Downloaded file seems too small: {file_size} bytes")
  139.  
  140.             self.log(f"Base image size: {file_size / (1024*1024):.1f} MB")
  141.             return image_path
  142.  
  143.         except Exception as e:
  144.             self.log(f"Failed to download base image: {e}", "ERROR")
  145.             if image_path.exists():
  146.                 image_path.unlink()
  147.             raise
  148.  
  149.     def create_instance_image(self, base_image):
  150.         instance_image = self.instance_dir / f'{self.instance_name}.qcow2'
  151.  
  152.         if instance_image.exists():
  153.             self.log(f"Removing existing instance image")
  154.             instance_image.unlink()
  155.  
  156.         self.log(f"Creating instance image with {self.vm_disk_size} disk")
  157.  
  158.         base_image_abs = base_image.resolve()
  159.         instance_image_abs = instance_image.resolve()
  160.  
  161.         cmd = [
  162.             'qemu-img', 'create', '-f', 'qcow2',
  163.             '-F', 'qcow2', '-b', str(base_image_abs),
  164.             str(instance_image_abs)
  165.         ]
  166.  
  167.         self.log(f"Base image path: {base_image_abs}")
  168.         self.log(f"Instance image path: {instance_image_abs}")
  169.  
  170.         result = subprocess.run(cmd, capture_output=True, text=True)
  171.         if result.returncode != 0:
  172.             raise Exception(f"Failed to create instance image: {result.stderr}")
  173.  
  174.         cmd = ['qemu-img', 'resize', str(instance_image_abs), self.vm_disk_size]
  175.         result = subprocess.run(cmd, capture_output=True, text=True)
  176.         if result.returncode != 0:
  177.             raise Exception(f"Failed to resize instance image: {result.stderr}")
  178.  
  179.         return instance_image
  180.  
  181.     def create_cloud_init_config(self):
  182.         user_data = {
  183.             'ssh_pwauth': True,
  184.             'disable_root': False,
  185.             'chpasswd': {
  186.                 'expire': False,
  187.                 'users': [
  188.                     {'name': 'root', 'password': self.ssh_password, 'type': 'text'},
  189.                     {'name': self.ssh_user, 'password': self.ssh_password, 'type': 'text'}
  190.                 ]
  191.             },
  192.  
  193.             'users': [
  194.                 {
  195.                     'name': self.ssh_user,
  196.                     'sudo': 'ALL=(ALL) NOPASSWD:ALL',
  197.                     'shell': '/bin/bash',
  198.                     'lock_passwd': False,
  199.                     'ssh_authorized_keys': self.config.get('ssh_keys', [])
  200.                 }
  201.             ],
  202.  
  203.             'package_update': True,
  204.             'packages': [
  205.                 'python3',
  206.                 'python3-pip',
  207.                 'curl',
  208.                 'wget',
  209.                 'htop',
  210.                 'tmux',
  211.                 'vim',
  212.                 'net-tools'
  213.             ],
  214.  
  215.             'runcmd': [
  216.                 'systemctl enable ssh',
  217.                 'systemctl start ssh',
  218.                 'systemctl status ssh',
  219.  
  220.                 'sed -i "s/#PasswordAuthentication yes/PasswordAuthentication yes/" /etc/ssh/sshd_config',
  221.                 'sed -i "s/PasswordAuthentication no/PasswordAuthentication yes/" /etc/ssh/sshd_config',
  222.                 'sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/" /etc/ssh/sshd_config',
  223.                 'systemctl restart ssh',
  224.  
  225.                 'touch /tmp/ssh_ready',
  226.                 'echo "SSH_READY_$(date +%s)" > /tmp/ssh_ready',
  227.  
  228.                 'mkdir -p /opt/ai-agent',
  229.                 f'chown {self.ssh_user}:{self.ssh_user} /opt/ai-agent',
  230.  
  231.                 f'echo "export PYTHONUNBUFFERED=1" >> /home/{self.ssh_user}/.bashrc',
  232.                 f'echo "export DEBIAN_FRONTEND=noninteractive" >> /home/{self.ssh_user}/.bashrc',
  233.                 f'echo "cd /opt/ai-agent" >> /home/{self.ssh_user}/.bashrc',
  234.  
  235.                 'nohup bash -c "sleep 60 && pip3 install requests aiohttp asyncio numpy pandas" > /tmp/pip_install.log 2>&1 &',
  236.  
  237.                 'echo "SYSTEM_READY_$(date +%s)" >> /tmp/ssh_ready',
  238.             ],
  239.  
  240.             'final_message': 'AI Agent VM is SSH-ready!'
  241.         }
  242.  
  243.         meta_data = {
  244.             'instance-id': f'{self.instance_name}-{int(time.time())}',
  245.             'local-hostname': self.instance_name
  246.         }
  247.  
  248.         network_config = {
  249.             'version': 2,
  250.             'ethernets': {
  251.                 'enp0s3': {
  252.                     'dhcp4': True
  253.                 }
  254.             }
  255.         }
  256.  
  257.         return user_data, meta_data, network_config
  258.  
  259.     def create_cloud_init_iso(self):
  260.         user_data, meta_data, network_config = self.create_cloud_init_config()
  261.  
  262.         iso_path = self.instance_dir / 'cloudinit.iso'
  263.  
  264.         with tempfile.TemporaryDirectory() as temp_dir:
  265.             temp_path = Path(temp_dir)
  266.  
  267.             with open(temp_path / 'user-data', 'w') as f:
  268.                 f.write('#cloud-config\n')
  269.                 yaml.dump(user_data, f, default_flow_style=False)
  270.  
  271.             with open(temp_path / 'meta-data', 'w') as f:
  272.                 yaml.dump(meta_data, f, default_flow_style=False)
  273.  
  274.             with open(temp_path / 'network-config', 'w') as f:
  275.                 yaml.dump(network_config, f, default_flow_style=False)
  276.  
  277.             cmd = [
  278.                 'genisoimage', '-output', str(iso_path),
  279.                 '-volid', 'cidata', '-joliet', '-rock',
  280.                 str(temp_path / 'user-data'),
  281.                 str(temp_path / 'meta-data'),
  282.                 str(temp_path / 'network-config')
  283.             ]
  284.  
  285.             result = subprocess.run(cmd, capture_output=True, text=True)
  286.             if result.returncode != 0:
  287.                 raise Exception(f"Failed to create cloud-init ISO: {result.stderr}")
  288.  
  289.         return iso_path
  290.  
  291.     def start_instance(self, instance_image, cloudinit_iso):
  292.         if self.is_running():
  293.             self.log("Instance is already running")
  294.             return True
  295.  
  296.         if self.serial_socket.exists():
  297.             self.serial_socket.unlink()
  298.  
  299.         cmd = [
  300.             'qemu-system-x86_64',
  301.             '-machine', 'accel=kvm:tcg',
  302.             '-cpu', 'host',
  303.             '-m', self.vm_memory,
  304.             '-smp', str(self.vm_cpus),
  305.  
  306.             '-drive', f'file={instance_image},format=qcow2,if=virtio',
  307.             '-drive', f'file={cloudinit_iso},format=raw,if=virtio,readonly=on',
  308.  
  309.             '-netdev', f'user,id=net0,hostfwd=tcp::{self.ssh_port}-:22',
  310.             '-device', 'virtio-net-pci,netdev=net0',
  311.  
  312.             '-serial', f'unix:{self.serial_socket},server,nowait',
  313.  
  314.             '-display', 'none',
  315.             '-daemonize',
  316.             '-pidfile', str(self.pid_file),
  317.             '-rtc', 'base=utc',
  318.             '-device', 'virtio-rng-pci',
  319.         ]
  320.  
  321.         self.log(f"Starting instance: {self.instance_name}")
  322.         self.log(f"SSH will be available on port {self.ssh_port}")
  323.  
  324.         result = subprocess.run(cmd, capture_output=True, text=True)
  325.         if result.returncode != 0:
  326.             raise Exception(f"Failed to start instance: {result.stderr}")
  327.  
  328.         self.log("Instance started successfully")
  329.         return True
  330.  
  331.     def wait_for_ssh(self, timeout=180):
  332.         self.log("Waiting for SSH to become available...")
  333.  
  334.         start_time = time.time()
  335.         last_status_time = time.time()
  336.  
  337.         while time.time() - start_time < timeout:
  338.             try:
  339.                 cmd = [
  340.                     'sshpass', '-p', self.ssh_password,
  341.                     'ssh', '-p', str(self.ssh_port),
  342.                     '-o', 'ConnectTimeout=3',
  343.                     '-o', 'StrictHostKeyChecking=no',
  344.                     '-o', 'UserKnownHostsFile=/dev/null',
  345.                     '-o', 'LogLevel=ERROR',
  346.                     f'{self.ssh_user}@localhost',
  347.                     'echo "SSH_TEST_SUCCESS"'
  348.                 ]
  349.  
  350.                 result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
  351.  
  352.                 if result.returncode == 0 and "SSH_TEST_SUCCESS" in result.stdout:
  353.                     elapsed = time.time() - start_time
  354.                     self.log(f"SSH is ready! (took {elapsed:.1f} seconds)")
  355.  
  356.                     with open(self.ssh_ready_flag, 'w') as f:
  357.                         f.write(f"ready:{int(time.time())}")
  358.  
  359.                     return True
  360.  
  361.             except subprocess.TimeoutExpired:
  362.                 pass
  363.             except Exception as e:
  364.                 pass
  365.  
  366.             if time.time() - last_status_time > 15:
  367.                 elapsed = time.time() - start_time
  368.                 self.log(f"Still waiting for SSH... ({elapsed:.0f}s elapsed)")
  369.                 last_status_time = time.time()
  370.  
  371.             time.sleep(2)
  372.  
  373.         self.log(f"SSH connection timeout after {timeout} seconds", "ERROR")
  374.         return False
  375.  
  376.     def test_ssh_connection(self):
  377.         try:
  378.             cmd = [
  379.                 'sshpass', '-p', self.ssh_password,
  380.                 'ssh', '-p', str(self.ssh_port),
  381.                 '-o', 'ConnectTimeout=5',
  382.                 '-o', 'StrictHostKeyChecking=no',
  383.                 '-o', 'UserKnownHostsFile=/dev/null',
  384.                 '-o', 'LogLevel=ERROR',
  385.                 f'{self.ssh_user}@localhost',
  386.                 'uname -a && echo "---" && free -h && echo "---" && df -h /'
  387.             ]
  388.  
  389.             result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
  390.  
  391.             if result.returncode == 0:
  392.                 return True, result.stdout.strip()
  393.             else:
  394.                 return False, result.stderr.strip()
  395.  
  396.         except Exception as e:
  397.             return False, str(e)
  398.  
  399.     def execute_ssh_command(self, command):
  400.         try:
  401.             cmd = [
  402.                 'sshpass', '-p', self.ssh_password,
  403.                 'ssh', '-p', str(self.ssh_port),
  404.                 '-o', 'ConnectTimeout=5',
  405.                 '-o', 'StrictHostKeyChecking=no',
  406.                 '-o', 'UserKnownHostsFile=/dev/null',
  407.                 '-o', 'LogLevel=ERROR',
  408.                 f'{self.ssh_user}@localhost',
  409.                 command
  410.             ]
  411.  
  412.             result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
  413.             return result.returncode == 0, result.stdout, result.stderr
  414.  
  415.         except Exception as e:
  416.             return False, "", str(e)
  417.  
  418.     def stop_instance(self):
  419.         if not self.is_running():
  420.             self.log("Instance is not running")
  421.             return True
  422.  
  423.         try:
  424.             with open(self.pid_file, 'r') as f:
  425.                 pid = int(f.read().strip())
  426.  
  427.             self.log(f"Stopping instance (PID: {pid})")
  428.  
  429.             os.kill(pid, 15)
  430.  
  431.             for _ in range(10):
  432.                 try:
  433.                     os.kill(pid, 0)
  434.                     time.sleep(1)
  435.                 except ProcessLookupError:
  436.                     break
  437.             else:
  438.                 self.log("Force killing instance")
  439.                 os.kill(pid, 9)
  440.  
  441.             if self.pid_file.exists():
  442.                 self.pid_file.unlink()
  443.             if self.serial_socket.exists():
  444.                 self.serial_socket.unlink()
  445.             if self.ssh_ready_flag.exists():
  446.                 self.ssh_ready_flag.unlink()
  447.  
  448.             self.log("Instance stopped successfully")
  449.             return True
  450.  
  451.         except Exception as e:
  452.             self.log(f"Error stopping instance: {e}", "ERROR")
  453.             return False
  454.  
  455.     def get_status(self):
  456.         status = {
  457.             'running': self.is_running(),
  458.             'ssh_ready': self.ssh_ready_flag.exists(),
  459.             'ssh_port': self.ssh_port,
  460.             'ssh_user': self.ssh_user,
  461.             'instance_dir': str(self.instance_dir)
  462.         }
  463.  
  464.         if status['running']:
  465.             with open(self.pid_file, 'r') as f:
  466.                 status['pid'] = int(f.read().strip())
  467.  
  468.         if status['ssh_ready']:
  469.             ssh_success, ssh_info = self.test_ssh_connection()
  470.             status['ssh_working'] = ssh_success
  471.             if ssh_success:
  472.                 status['system_info'] = ssh_info
  473.  
  474.         return status
  475.  
  476.     def run(self, force_recreate=False):
  477.         try:
  478.             if self.is_running() and not force_recreate:
  479.                 self.log("Instance is already running")
  480.                 if self.ssh_ready_flag.exists():
  481.                     self.log(f"SSH ready on port {self.ssh_port}")
  482.                     return True
  483.                 else:
  484.                     self.log("Waiting for SSH to become ready...")
  485.                     return self.wait_for_ssh()
  486.  
  487.             if force_recreate:
  488.                 self.stop_instance()
  489.  
  490.             if not self.check_dependencies():
  491.                 return False
  492.  
  493.             base_image = self.download_base_image()
  494.             instance_image = self.create_instance_image(base_image)
  495.             cloudinit_iso = self.create_cloud_init_iso()
  496.             self.start_instance(instance_image, cloudinit_iso)
  497.  
  498.             if self.wait_for_ssh():
  499.                 self.log("Instance is ready for AI agent connection!")
  500.                 return True
  501.             else:
  502.                 self.log("Instance started but SSH is not ready", "ERROR")
  503.                 return False
  504.  
  505.         except Exception as e:
  506.             self.log(f"Failed to run instance: {e}", "ERROR")
  507.             return False
  508.  
  509. def load_config(config_file=None):
  510.     default_config = {
  511.         'instance_name': 'ai-agent',
  512.         'memory': '4G',
  513.         'cpus': 2,
  514.         'disk_size': '30G',
  515.         'ssh_user': 'aiagent',
  516.         'ssh_password': 'aiagent123',
  517.         'ssh_port': 2222,
  518.         'ssh_keys': [],
  519.         'work_dir': './vm_instances'
  520.     }
  521.  
  522.     if config_file and os.path.exists(config_file):
  523.         with open(config_file, 'r') as f:
  524.             if config_file.endswith('.json'):
  525.                 user_config = json.load(f)
  526.             else:
  527.                 user_config = yaml.safe_load(f)
  528.         default_config.update(user_config)
  529.  
  530.     return default_config
  531.  
  532. def main():
  533.     parser = argparse.ArgumentParser(description='AI Agent VM Manager')
  534.     parser.add_argument('command', nargs='?', choices=['run', 'stop', 'status', 'connect', 'exec'],
  535.                        default='run', help='Command to execute')
  536.     parser.add_argument('--name', '-n', help='Instance name', default='ai-agent')
  537.     parser.add_argument('--config', '-c', help='Configuration file')
  538.     parser.add_argument('--force', '-f', action='store_true', help='Force recreate instance')
  539.     parser.add_argument('--port', '-p', type=int, help='SSH port override')
  540.     parser.add_argument('command_args', nargs='*', help='Additional command arguments')
  541.  
  542.     args = parser.parse_args()
  543.  
  544.     config = load_config(args.config)
  545.     config['instance_name'] = args.name
  546.  
  547.     if args.port:
  548.         config['ssh_port'] = args.port
  549.  
  550.     manager = AIVMManager(config)
  551.  
  552.     if args.command == 'run':
  553.         success = manager.run(force_recreate=args.force)
  554.         if success:
  555.             print(f"\n=== INSTANCE READY ===")
  556.             print(f"SSH: ssh -p {config['ssh_port']} {config['ssh_user']}@localhost")
  557.             print(f"Password: {config['ssh_password']}")
  558.             print(f"Instance directory: {manager.instance_dir}")
  559.             print("======================")
  560.         sys.exit(0 if success else 1)
  561.  
  562.     elif args.command == 'stop':
  563.         success = manager.stop_instance()
  564.         sys.exit(0 if success else 1)
  565.  
  566.     elif args.command == 'status':
  567.         status = manager.get_status()
  568.         print("=== INSTANCE STATUS ===")
  569.         for key, value in status.items():
  570.             print(f"{key}: {value}")
  571.         print("=======================")
  572.  
  573.     elif args.command == 'connect':
  574.         if not manager.is_running():
  575.             print("Instance is not running. Use 'run' command first.")
  576.             sys.exit(1)
  577.  
  578.         ssh_cmd = [
  579.             'sshpass', '-p', config['ssh_password'],
  580.             'ssh', '-p', str(config['ssh_port']),
  581.             '-o', 'StrictHostKeyChecking=no',
  582.             '-o', 'UserKnownHostsFile=/dev/null',
  583.             f"{config['ssh_user']}@localhost"
  584.         ]
  585.  
  586.         print(f"Connecting to {config['instance_name']}...")
  587.         subprocess.run(ssh_cmd)
  588.  
  589.     elif args.command == 'exec':
  590.         if not args.command_args:
  591.             print("No command specified for exec")
  592.             sys.exit(1)
  593.  
  594.         command = ' '.join(args.command_args)
  595.         success, stdout, stderr = manager.execute_ssh_command(command)
  596.  
  597.         if stdout:
  598.             print(stdout)
  599.         if stderr:
  600.             print(stderr, file=sys.stderr)
  601.  
  602.         sys.exit(0 if success else 1)
  603.  
  604. if __name__ == "__main__":
  605.     main()
  606.  
Advertisement
Add Comment
Please, Sign In to add comment