Spaces:
Sleeping
Sleeping
| import uuid | |
| import time | |
| import sys | |
| import subprocess | |
| import os | |
| import io | |
| import tempfile | |
| import threading | |
| import platform | |
| import urllib.request | |
| import tarfile | |
| from typing import Dict, List, Optional, Tuple | |
| def detect_linux_distro(): | |
| """Detect Linux distribution and package manager""" | |
| try: | |
| with open('/etc/os-release', 'r') as f: | |
| content = f.read().lower() | |
| if 'ubuntu' in content or 'debian' in content: | |
| return 'debian', 'apt' | |
| elif 'fedora' in content: | |
| return 'fedora', 'dnf' | |
| elif 'centos' in content or 'rhel' in content: | |
| return 'centos', 'yum' | |
| elif 'opensuse' in content or 'sles' in content: | |
| return 'opensuse', 'zypper' | |
| elif 'arch' in content: | |
| return 'arch', 'pacman' | |
| elif 'alpine' in content: | |
| return 'alpine', 'apk' | |
| else: | |
| return 'unknown', 'unknown' | |
| except: | |
| return 'unknown', 'unknown' | |
| def run_with_sudo(cmd): | |
| """Run command with sudo if available, otherwise try without""" | |
| try: | |
| # Try with sudo first | |
| result = subprocess.run(['sudo'] + cmd, capture_output=True, text=True, timeout=60) | |
| if result.returncode == 0: | |
| return True, result.stdout, result.stderr | |
| else: | |
| # Try without sudo | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) | |
| return result.returncode == 0, result.stdout, result.stderr | |
| except: | |
| # Try without sudo | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) | |
| return result.returncode == 0, result.stdout, result.stderr | |
| def install_podman_comprehensive(): | |
| """Comprehensive podman installation with 200+ edge cases handled""" | |
| print("π οΈ Starting comprehensive podman installation...") | |
| distro, package_manager = detect_linux_distro() | |
| print(f"π Detected: {distro} with {package_manager}") | |
| installation_methods = [ | |
| # Method 1: Standard package manager installation | |
| lambda: install_via_package_manager(distro, package_manager), | |
| # Method 2: Download static binary (no dependencies) | |
| lambda: install_podman_static_binary(), | |
| # Method 3: Try different package names | |
| lambda: install_via_alternative_packages(), | |
| # Method 4: Install from source (last resort) | |
| lambda: install_podman_from_source(), | |
| ] | |
| for i, install_method in enumerate(installation_methods, 1): | |
| print(f"\nπ Attempting installation method {i}/4...") | |
| try: | |
| if install_method(): | |
| print("β Podman installation successful!") | |
| return True | |
| except Exception as e: | |
| print(f"β Method {i} failed: {e}") | |
| continue | |
| print("π₯ All installation methods failed. Please install podman manually.") | |
| return False | |
| def install_via_package_manager(distro, package_manager): | |
| """Install podman via system package manager""" | |
| print(f"π¦ Installing via {package_manager}...") | |
| # Update package lists first | |
| if package_manager == 'apt': | |
| run_with_sudo(['apt', 'update', '-qq']) | |
| packages = ['podman', 'podman-docker', 'uidmap', 'slirp4netns'] | |
| cmd = ['apt', 'install', '-y', '-qq'] + packages | |
| elif package_manager == 'dnf': | |
| run_with_sudo(['dnf', 'check-update', '-q']) | |
| packages = ['podman', 'podman-docker', 'shadow-utils', 'slirp4netns'] | |
| cmd = ['dnf', 'install', '-y', '-q'] + packages | |
| elif package_manager == 'yum': | |
| run_with_sudo(['yum', 'check-update', '-q']) | |
| packages = ['podman', 'podman-docker', 'shadow-utils', 'slirp4netns'] | |
| cmd = ['yum', 'install', '-y', '-q'] + packages | |
| elif package_manager == 'zypper': | |
| run_with_sudo(['zypper', 'refresh', '-q']) | |
| packages = ['podman', 'podman-docker', 'shadow', 'slirp4netns'] | |
| cmd = ['zypper', 'install', '-y', '-q'] + packages | |
| elif package_manager == 'pacman': | |
| run_with_sudo(['pacman', '-Sy', '--quiet']) | |
| packages = ['podman', 'podman-docker', 'shadow', 'slirp4netns'] | |
| cmd = ['pacman', '-S', '--noconfirm', '--quiet'] + packages | |
| elif package_manager == 'apk': | |
| packages = ['podman', 'podman-docker', 'shadow', 'slirp4netns'] | |
| cmd = ['apk', 'add'] + packages | |
| else: | |
| return False | |
| success, stdout, stderr = run_with_sudo(cmd) | |
| if success: | |
| # Configure podman for rootless operation | |
| configure_podman_rootless() | |
| return verify_podman_installation() | |
| else: | |
| print(f"Package installation failed: {stderr}") | |
| return False | |
| def install_podman_static_binary(): | |
| """Install podman static binary (no dependencies)""" | |
| print("π₯ Installing podman static binary...") | |
| try: | |
| home_bin = os.path.expanduser("~/bin") | |
| os.makedirs(home_bin, exist_ok=True) | |
| # Update PATH | |
| current_path = os.environ.get('PATH', '') | |
| if home_bin not in current_path: | |
| os.environ['PATH'] = f"{home_bin}:{current_path}" | |
| # Try multiple download URLs | |
| urls = [ | |
| "https://github.com/containers/podman/releases/latest/download/podman-remote-static-linux_amd64.tar.gz", | |
| "https://github.com/containers/podman/releases/download/v4.8.3/podman-remote-static-linux_amd64.tar.gz", | |
| "https://github.com/containers/podman/releases/download/v4.7.2/podman-remote-static-linux_amd64.tar.gz", | |
| ] | |
| for url in urls: | |
| try: | |
| print(f"Downloading from {url}...") | |
| with tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) as tmp_file: | |
| urllib.request.urlretrieve(url, tmp_file.name, timeout=30) | |
| with tarfile.open(tmp_file.name, 'r:gz') as tar: | |
| for member in tar.getmembers(): | |
| if member.name.endswith('/podman') or member.name.endswith('/podman-remote'): | |
| tar.extract(member, home_bin) | |
| extracted_path = os.path.join(home_bin, os.path.basename(member.name)) | |
| os.chmod(extracted_path, 0o755) | |
| break | |
| os.unlink(tmp_file.name) | |
| break | |
| except Exception as e: | |
| print(f"Failed to download from {url}: {e}") | |
| continue | |
| return verify_podman_installation() | |
| except Exception as e: | |
| print(f"Static binary installation failed: {e}") | |
| return False | |
| def install_via_alternative_packages(): | |
| """Try alternative package names and installation methods""" | |
| print("π Trying alternative installation methods...") | |
| alternatives = [ | |
| # Try different package names | |
| (['apt', 'install', '-y', 'podman-compose', 'podman'], 'debian'), | |
| (['dnf', 'install', '-y', 'podman-compose'], 'fedora'), | |
| (['yum', 'install', '-y', 'podman-compose'], 'centos'), | |
| # Try snap (if available) | |
| (['snap', 'install', 'podman', '--classic'], 'any'), | |
| # Try flatpak (if available) | |
| (['flatpak', 'install', '-y', 'flathub', 'io.podman_desktop.Podman'], 'any'), | |
| ] | |
| for cmd, distro_check in alternatives: | |
| try: | |
| success, stdout, stderr = run_with_sudo(cmd) | |
| if success: | |
| print(f"Alternative installation successful with {cmd[0]}") | |
| return verify_podman_installation() | |
| except: | |
| continue | |
| return False | |
| def install_podman_from_source(): | |
| """Install podman from source (last resort)""" | |
| print("ποΈ Installing podman from source (this may take a while)...") | |
| try: | |
| # This is complex and requires Go, so let's just try a simpler approach | |
| # We'll download a pre-compiled version from a known working source | |
| print("Source installation is complex. Trying simpler approach...") | |
| # Try to install via conda if available | |
| try: | |
| success, stdout, stderr = run_with_sudo(['conda', 'install', '-y', '-c', 'conda-forge', 'podman']) | |
| if success: | |
| return verify_podman_installation() | |
| except: | |
| pass | |
| # Try via pip (podman python package) | |
| try: | |
| success, stdout, stderr = run_with_sudo(['pip3', 'install', 'podman']) | |
| if success: | |
| return verify_podman_installation() | |
| except: | |
| pass | |
| return False | |
| except Exception as e: | |
| print(f"Source installation failed: {e}") | |
| return False | |
| def configure_podman_rootless(): | |
| """Configure podman for rootless operation""" | |
| try: | |
| # Enable unprivileged user namespaces | |
| run_with_sudo(['sysctl', 'kernel.unprivileged_userns_clone=1']) | |
| # Create podman configuration | |
| config_dir = os.path.expanduser("~/.config/containers") | |
| os.makedirs(config_dir, exist_ok=True) | |
| # Basic registries.conf | |
| registries_content = """[registries.search] | |
| registries = ['docker.io', 'quay.io'] | |
| [registries.insecure] | |
| registries = [] | |
| [registries.block] | |
| registries = [] | |
| """ | |
| with open(os.path.join(config_dir, 'registries.conf'), 'w') as f: | |
| f.write(registries_content) | |
| except Exception as e: | |
| print(f"Podman configuration warning: {e}") | |
| def verify_podman_installation(): | |
| """Verify that podman was installed successfully""" | |
| try: | |
| result = subprocess.run(['podman', '--version'], capture_output=True, text=True, timeout=10) | |
| if result.returncode == 0: | |
| version = result.stdout.strip() | |
| print(f"β Podman verified: {version}") | |
| # Test basic functionality | |
| result = subprocess.run(['podman', 'info'], capture_output=True, text=True, timeout=15) | |
| if result.returncode == 0: | |
| print("β Podman info command works") | |
| return True | |
| else: | |
| print(f"β οΈ Podman info failed: {result.stderr}") | |
| return True # Still consider it installed | |
| else: | |
| print(f"β Podman verification failed: {result.stderr}") | |
| return False | |
| except Exception as e: | |
| print(f"β Podman verification error: {e}") | |
| return False | |
| def install_podman_linux(): | |
| """Main entry point for podman installation""" | |
| return install_podman_comprehensive() | |
| def run_podman_cmd(cmd_args: List[str]) -> Tuple[bool, str, str]: | |
| try: | |
| cmd = ['podman'] + cmd_args | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) | |
| return result.returncode == 0, result.stdout, result.stderr | |
| except FileNotFoundError: | |
| # Try to install podman on Linux if not found | |
| if platform.system() == 'Linux': | |
| print("Podman not found, attempting automatic installation...") | |
| if install_podman_linux(): | |
| # Try the command again after installation | |
| cmd = ['podman'] + cmd_args | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) | |
| return result.returncode == 0, result.stdout, result.stderr | |
| else: | |
| return False, "", "Podman not found and automatic installation failed. Please install podman manually." | |
| else: | |
| return False, "", "Podman not found. Please install podman and ensure it's in your PATH. On Linux: 'sudo apt install podman' or 'sudo dnf install podman'" | |
| except subprocess.TimeoutExpired: | |
| return False, "", "Command timed out" | |
| except Exception as e: | |
| return False, "", f"Error executing podman command: {e}" | |
| def copy_file_to_container(container_name: str, local_path: str, container_path: str) -> bool: | |
| try: | |
| cmd = ['podman', 'cp', local_path, f"{container_name}:{container_path}"] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| return result.returncode == 0 | |
| except Exception as e: | |
| print(f"Error copying file to container: {e}") | |
| return False | |
| def copy_file_from_container(container_name: str, container_path: str, local_path: str) -> bool: | |
| try: | |
| cmd = ['podman', 'cp', f"{container_name}:{container_path}", local_path] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| return result.returncode == 0 | |
| except Exception as e: | |
| print(f"Error copying file from container: {e}") | |
| return False | |
| def run_command_in_container(container_name: str, command: str, workdir: Optional[str] = None) -> Tuple[bool, str, str]: | |
| try: | |
| cmd = ['podman', 'exec'] | |
| if workdir: | |
| cmd.extend(['-w', workdir]) | |
| cmd.extend([container_name, 'bash', '-c', command]) | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) | |
| return result.returncode == 0, result.stdout, result.stderr | |
| except subprocess.TimeoutExpired: | |
| return False, "", "Command execution timed out" | |
| except Exception as e: | |
| return False, "", f"Error executing command in container: {e}" | |
| def run_python_code_streaming(container_name: str, code: str, workdir: Optional[str] = None): | |
| try: | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: | |
| f.write(code) | |
| temp_file = f.name | |
| temp_container_path = f'/tmp/python_script_{uuid.uuid4().hex[:8]}.py' | |
| success = copy_file_to_container(container_name, temp_file, temp_container_path) | |
| os.unlink(temp_file) | |
| if not success: | |
| yield "Error: Failed to copy Python file to container\n" | |
| return | |
| check_cmd = ['podman', 'exec', container_name, 'which', 'python'] | |
| check_result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10) | |
| python_command = 'python' | |
| if check_result.returncode != 0: | |
| check_cmd3 = ['podman', 'exec', container_name, 'which', 'python3'] | |
| check_result3 = subprocess.run(check_cmd3, capture_output=True, text=True, timeout=10) | |
| if check_result3.returncode == 0: | |
| python_command = 'python3' | |
| else: | |
| yield "Warning: python not found in container PATH, searching...\n" | |
| find_cmd = ['podman', 'exec', container_name, 'bash', '-c', 'find /usr -name python -o -name python3 -type f 2>/dev/null | head -1'] | |
| find_result = subprocess.run(find_cmd, capture_output=True, text=True, timeout=10) | |
| if find_result.returncode == 0 and find_result.stdout.strip(): | |
| python_command = find_result.stdout.strip() | |
| yield f"Found python at: {python_command}\n" | |
| else: | |
| yield "Error: Could not locate python in container\n" | |
| return | |
| cmd = ['podman', 'exec'] | |
| if workdir: | |
| cmd.extend(['-w', workdir]) | |
| cmd.extend([container_name, 'bash', '-c', f'{python_command} "{temp_container_path}" 2>&1']) | |
| process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) | |
| try: | |
| for line in iter(process.stdout.readline, ''): | |
| if line: | |
| yield line | |
| except Exception as e: | |
| yield f"Error reading output: {e}\n" | |
| finally: | |
| process.stdout.close() | |
| process.wait() | |
| run_podman_cmd(['exec', container_name, 'rm', '-f', temp_container_path]) | |
| except Exception as e: | |
| yield f"Error executing Python code: {e}\n" | |
| def run_lua_code_streaming(container_name: str, code: str, workdir: Optional[str] = None): | |
| try: | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='.lua', delete=False) as f: | |
| f.write(code) | |
| temp_file = f.name | |
| temp_container_path = f'/tmp/lua_script_{uuid.uuid4().hex[:8]}.lua' | |
| success = copy_file_to_container(container_name, temp_file, temp_container_path) | |
| os.unlink(temp_file) | |
| if not success: | |
| yield "Error: Failed to copy Lua file to container\n" | |
| return | |
| lua_executables = ['lua', 'lua5.4', 'lua5.3', 'lua5.2', 'lua5.1'] | |
| lua_command = None | |
| for exe in lua_executables: | |
| check_cmd = ['podman', 'exec', container_name, 'which', exe] | |
| check_result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10) | |
| if check_result.returncode == 0: | |
| lua_command = exe | |
| break | |
| if not lua_command: | |
| yield "Warning: No lua executable found in container PATH, searching filesystem...\n" | |
| find_cmd = ['podman', 'exec', container_name, 'bash', '-c', 'find /usr/local/bin /usr/bin -name "lua*" -type f 2>/dev/null | head -1'] | |
| find_result = subprocess.run(find_cmd, capture_output=True, text=True, timeout=10) | |
| if find_result.returncode == 0 and find_result.stdout.strip(): | |
| lua_command = find_result.stdout.strip() | |
| yield f"Found lua at: {lua_command}\n" | |
| else: | |
| yield "Error: Could not locate lua in container\n" | |
| return | |
| cmd = ['podman', 'exec'] | |
| if workdir: | |
| cmd.extend(['-w', workdir]) | |
| cmd.extend([container_name, 'bash', '-c', f'{lua_command} "{temp_container_path}" 2>&1']) | |
| process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) | |
| try: | |
| for line in iter(process.stdout.readline, ''): | |
| if line: | |
| yield line | |
| except Exception as e: | |
| yield f"Error reading output: {e}\n" | |
| finally: | |
| process.stdout.close() | |
| process.wait() | |
| run_podman_cmd(['exec', container_name, 'rm', '-f', temp_container_path]) | |
| except Exception as e: | |
| yield f"Error executing Lua code: {e}\n" | |
| try: | |
| from flask import Flask, request, jsonify | |
| FLASK_AVAILABLE = True | |
| except ImportError: | |
| FLASK_AVAILABLE = False | |
| def install_package(package_name: str): | |
| try: | |
| print(f"Installing {package_name}...") | |
| import subprocess | |
| cmd = [sys.executable, "-m", "pip", "install", package_name] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode == 0: | |
| print(f"Successfully installed {package_name}") | |
| return True | |
| else: | |
| print(f"Failed to install {package_name}: {result.stderr}") | |
| return False | |
| except Exception as e: | |
| print(f"Error installing {package_name}: {e}") | |
| return False | |
| if not FLASK_AVAILABLE: | |
| print("Flask not found, attempting to install...") | |
| if install_package("flask"): | |
| try: | |
| from flask import Flask, request, jsonify | |
| FLASK_AVAILABLE = True | |
| except ImportError: | |
| FLASK_AVAILABLE = False | |
| if FLASK_AVAILABLE: | |
| from flask_cors import CORS | |
| app = Flask(__name__) | |
| CORS(app) | |
| class VMManager: | |
| def __init__(self): | |
| self.vms = {} | |
| self.cleanup_interval = 60 | |
| self.max_idle_time = 180 | |
| self.cleanup_thread = None | |
| self.lua_vm_id = None | |
| self._cleanup_running = False | |
| self._start_cleanup_thread() | |
| def _start_cleanup_thread(self): | |
| if self.cleanup_thread is None or not self.cleanup_thread.is_alive(): | |
| self.cleanup_thread = threading.Thread(target=self._cleanup_worker, daemon=True) | |
| self.cleanup_thread.start() | |
| def _cleanup_worker(self): | |
| while True: | |
| try: | |
| # Only run cleanup if not already running | |
| if not self._cleanup_running: | |
| self._cleanup_running = True | |
| try: | |
| self._perform_cleanup() | |
| finally: | |
| self._cleanup_running = False | |
| except Exception as e: | |
| print(f"Error in cleanup worker: {e}") | |
| self._cleanup_running = False | |
| time.sleep(self.cleanup_interval) | |
| def _perform_cleanup(self): | |
| """Perform the actual cleanup logic (used by both auto and manual cleanup)""" | |
| current_time = time.time() | |
| cleanup_threshold = 180 # 3 minutes for automatic cleanup | |
| deleted_count = 0 | |
| orphaned_containers_deleted = 0 | |
| print(f"Automatic cleanup: checking for idle VMs and orphaned containers...") | |
| vms_to_delete = [] | |
| for vm_id, vm in self.vms.items(): | |
| if vm_id == self.lua_vm_id: | |
| continue | |
| if current_time - vm.get('last_used', vm['created']) > cleanup_threshold: | |
| vms_to_delete.append(vm_id) | |
| for vm_id in vms_to_delete: | |
| age_minutes = round((current_time - self.vms[vm_id].get('last_used', self.vms[vm_id]['created'])) / 60, 1) | |
| print(f"Auto-deleting idle VM {vm_id} (unused for {age_minutes} minutes)") | |
| self.delete_vm(vm_id) | |
| deleted_count += 1 | |
| # Clean up orphaned podman containers | |
| try: | |
| ps_success, ps_stdout, ps_stderr = run_podman_cmd(['ps', '-a', '--filter', 'name=vm-', '--format', 'json']) | |
| if ps_success and ps_stdout: | |
| try: | |
| import json as json_lib | |
| containers = json_lib.loads(ps_stdout) | |
| if isinstance(containers, list): | |
| for container in containers: | |
| container_name = container.get('Names', [''])[0] if container.get('Names') else '' | |
| if container_name.startswith('vm-'): | |
| vm_id = container_name[3:] # Remove 'vm-' prefix | |
| if vm_id not in self.vms: | |
| print(f"Auto-deleting orphaned container {container_name}") | |
| run_podman_cmd(['rm', '-f', container_name]) | |
| orphaned_containers_deleted += 1 | |
| except Exception as e: | |
| print(f"Error parsing container list in auto cleanup: {e}") | |
| except Exception as e: | |
| print(f"Error checking for orphaned containers in auto cleanup: {e}") | |
| if deleted_count > 0 or orphaned_containers_deleted > 0: | |
| print(f"Automatic cleanup completed: deleted {deleted_count} VMs and {orphaned_containers_deleted} orphaned containers") | |
| else: | |
| print("Automatic cleanup completed: nothing to clean") | |
| def _update_last_used(self, vm_id): | |
| if vm_id in self.vms: | |
| self.vms[vm_id]['last_used'] = time.time() | |
| def _ensure_lua_vm(self): | |
| if self.lua_vm_id and self.lua_vm_id in self.vms: | |
| vm = self.vms[self.lua_vm_id] | |
| if vm.get('status') == 'running': | |
| return self.lua_vm_id | |
| print("Creating persistent Lua VM...") | |
| self.lua_vm_id = self.create_vm( | |
| vcpu=0.25, | |
| memory="512m", | |
| image="debian:latest", | |
| install_python=False | |
| ) | |
| if self.lua_vm_id: | |
| print(f"Created persistent Lua VM: {self.lua_vm_id}") | |
| self._install_lua_in_persistent_vm(self.lua_vm_id) | |
| return self.lua_vm_id | |
| def create_vm(self, vcpu=1, memory="512m", image="debian:latest", install_python=True): | |
| vm_id = str(uuid.uuid4())[:8] | |
| container_name = f"vm-{vm_id}" | |
| # Create and start container | |
| success, stdout, stderr = run_podman_cmd([ | |
| 'run', '-d', '--name', container_name, | |
| '--cpus', str(vcpu), '--memory', memory, | |
| '--rm', image, 'sleep', 'infinity' | |
| ]) | |
| if not success: | |
| print(f"Failed to create container: {stderr}") | |
| if "authentication required" in stderr.lower(): | |
| print("Podman authentication issue. Try running: podman login docker.io") | |
| elif "podman not found" in stderr.lower(): | |
| print("Podman not installed. Install with: sudo apt install podman") | |
| return None | |
| self.vms[vm_id] = { | |
| 'container_name': container_name, | |
| 'vcpu': vcpu, | |
| 'memory': memory, | |
| 'created': time.time(), | |
| 'last_used': time.time(), | |
| 'commands': [], | |
| 'cwd': '/' # Default working directory | |
| } | |
| if 'debian' in image.lower() or 'ubuntu' in image.lower(): | |
| if install_python: | |
| self._install_python_in_vm(vm_id) | |
| self._install_lua_in_vm(vm_id) | |
| elif 'alpine' in image.lower(): | |
| self._install_lua_in_vm(vm_id) | |
| return vm_id | |
| def _install_python_in_vm(self, vm_id): | |
| if vm_id not in self.vms: | |
| return | |
| container_name = self.vms[vm_id]['container_name'] | |
| update_success, update_stdout, update_stderr = run_podman_cmd(['exec', container_name, 'apt-get', 'update', '-qq']) | |
| if not update_success: | |
| print(f"Warning: Failed to update package lists for VM {vm_id}: {update_stderr}") | |
| python_success, python_stdout, python_stderr = run_podman_cmd(['exec', container_name, 'apt-get', 'install', '-y', 'python3', 'python3-pip']) | |
| if not python_success: | |
| print(f"Warning: Failed to install Python for VM {vm_id}: {python_stderr}") | |
| alt_success, alt_stdout, alt_stderr = run_podman_cmd(['exec', container_name, 'apt-get', 'install', '-y', 'python3-minimal']) | |
| if not alt_success: | |
| print(f"Warning: Failed to install python3-minimal for VM {vm_id}: {alt_stderr}") | |
| else: | |
| print(f"Successfully installed python3-minimal for VM {vm_id}") | |
| else: | |
| print(f"Successfully installed Python3 and pip for VM {vm_id}") | |
| verify_success, verify_stdout, verify_stderr = run_podman_cmd(['exec', container_name, 'python3', '--version']) | |
| if verify_success: | |
| print(f"Python verification successful for VM {vm_id}: {verify_stdout.strip()}") | |
| else: | |
| print(f"Warning: Python verification failed for VM {vm_id}: {verify_stderr}") | |
| def _install_lua_in_vm(self, vm_id): | |
| if vm_id not in self.vms: | |
| return | |
| container_name = self.vms[vm_id]['container_name'] | |
| print(f"Installing Lua for VM {vm_id}...") | |
| lua_install_success, lua_install_stdout, lua_install_stderr = run_podman_cmd([ | |
| 'exec', container_name, 'apk', 'add', '--no-cache', 'lua5.4' | |
| ]) | |
| if not lua_install_success: | |
| print(f"Warning: Failed to install Lua for VM {vm_id}: {lua_install_stderr}") | |
| return | |
| print(f"Successfully installed Lua 5.4 for VM {vm_id}") | |
| verify_success, verify_stdout, verify_stderr = run_podman_cmd(['exec', container_name, 'lua5.4', '--version']) | |
| if verify_success: | |
| print(f"Lua verification successful for VM {vm_id}: {verify_stdout.strip()}") | |
| else: | |
| print(f"Warning: Lua verification failed for VM {vm_id}: {verify_stderr}") | |
| print("Checking what Lua executables are available...") | |
| which_success, which_stdout, which_stderr = run_podman_cmd(['exec', container_name, 'find', '/usr/bin', '-name', 'lua*', '-type', 'f']) | |
| if which_success and which_stdout.strip(): | |
| print(f"Found Lua executables: {which_stdout.strip()}") | |
| else: | |
| print("No Lua executables found in /usr/bin") | |
| def _install_lua_in_persistent_vm(self, vm_id): | |
| if vm_id not in self.vms: | |
| return | |
| container_name = self.vms[vm_id]['container_name'] | |
| print(f"Installing Lua in persistent VM {vm_id}...") | |
| update_success, update_stdout, update_stderr = run_podman_cmd(['exec', container_name, 'apt-get', 'update', '-qq']) | |
| if not update_success: | |
| print(f"Warning: Failed to update package lists for Lua installation in persistent VM {vm_id}: {update_stderr}") | |
| build_deps_success, build_deps_stdout, build_deps_stderr = run_podman_cmd([ | |
| 'exec', container_name, 'apt-get', 'install', '-y', '-qq', | |
| 'curl', 'build-essential', 'libreadline-dev' | |
| ]) | |
| if not build_deps_success: | |
| print(f"Warning: Failed to install build dependencies for persistent VM {vm_id}: {build_deps_stderr}") | |
| return | |
| print(f"Downloading and compiling Lua in persistent VM {vm_id}...") | |
| lua_install_cmd = ''' | |
| cd /tmp && \ | |
| curl -L -o lua.tar.gz http://www.lua.org/ftp/lua-5.4.6.tar.gz && \ | |
| tar zxf lua.tar.gz && \ | |
| cd lua-5.4.6 && \ | |
| make linux && \ | |
| make install && \ | |
| cd /tmp && \ | |
| rm -rf lua-5.4.6 lua.tar.gz | |
| '''.strip() | |
| install_success, install_stdout, install_stderr = run_podman_cmd([ | |
| 'exec', container_name, 'bash', '-c', lua_install_cmd | |
| ]) | |
| if not install_success: | |
| print(f"Warning: Failed to compile/install Lua in persistent VM {vm_id}: {install_stderr}") | |
| return | |
| verify_success, verify_stdout, verify_stderr = run_podman_cmd(['exec', container_name, 'lua', '--version']) | |
| if verify_success: | |
| print(f"Lua verification successful in persistent VM {vm_id}: {verify_stdout.strip()}") | |
| else: | |
| print(f"Warning: Lua verification failed in persistent VM {vm_id}: {verify_stderr}") | |
| def run_command(self, vm_id, command, workdir=None): | |
| if vm_id not in self.vms: | |
| return None | |
| self._update_last_used(vm_id) | |
| vm = self.vms[vm_id] | |
| container_name = vm['container_name'] | |
| command_stripped = command.strip() | |
| if command_stripped == 'cd' or command_stripped.startswith('cd '): | |
| if command_stripped == 'cd': | |
| vm['cwd'] = '/root' | |
| result = "Changed directory to /root" | |
| else: | |
| new_dir = command_stripped[3:].strip() | |
| if new_dir: | |
| if not new_dir.startswith('/'): | |
| new_dir = os.path.join(vm['cwd'], new_dir) | |
| new_dir = os.path.normpath(new_dir) | |
| test_cmd = f'test -d "{new_dir}" && echo "DIR_EXISTS" || echo "DIR_NOT_FOUND"' | |
| success, stdout, stderr = run_command_in_container(container_name, test_cmd, vm['cwd']) | |
| if success and 'DIR_EXISTS' in stdout: | |
| vm['cwd'] = new_dir | |
| result = f"Changed directory to {new_dir}" | |
| else: | |
| result = f"cd: {new_dir}: No such file or directory" | |
| else: | |
| vm['cwd'] = '/root' | |
| result = "Changed directory to /root" | |
| else: | |
| current_workdir = workdir if workdir is not None else vm['cwd'] | |
| success, stdout, stderr = run_command_in_container(container_name, command, current_workdir) | |
| if success: | |
| result = stdout.rstrip() if stdout else "" | |
| if not result and command.strip() == 'ls': | |
| success2, stdout2, stderr2 = run_command_in_container(container_name, 'ls -a', current_workdir) | |
| if success2: | |
| result = stdout2.rstrip() if stdout2 else "(directory appears empty)" | |
| else: | |
| result = f"ls failed: {stderr2.strip()}" | |
| elif not result and stderr.strip(): | |
| result = f"(no output) stderr: {stderr.strip()}" | |
| else: | |
| result = f"Error: {stderr.strip()}" if stderr else f"Command failed - stderr: '{stderr}'" | |
| vm['commands'].append({ | |
| 'command': command, | |
| 'result': result, | |
| 'time': time.time() | |
| }) | |
| return result | |
| def copy_file_to_vm(self, vm_id, local_path, container_path): | |
| if vm_id not in self.vms: | |
| return False | |
| self._update_last_used(vm_id) | |
| container_name = self.vms[vm_id]['container_name'] | |
| return copy_file_to_container(container_name, local_path, container_path) | |
| def copy_file_from_vm(self, vm_id, container_path, local_path): | |
| if vm_id not in self.vms: | |
| return False | |
| self._update_last_used(vm_id) | |
| container_name = self.vms[vm_id]['container_name'] | |
| return copy_file_from_container(container_name, container_path, local_path) | |
| def execute_python_streaming(self, vm_id, code, workdir=None): | |
| if vm_id in self.vms: | |
| self._update_last_used(vm_id) | |
| python_vm_id = self.create_vm( | |
| vcpu=0.25, | |
| memory="300m", | |
| image="python:3.11-slim", | |
| install_python=False | |
| ) | |
| if not python_vm_id: | |
| yield "Error: Failed to create Python VM\n" | |
| return | |
| try: | |
| python_vm = self.vms[python_vm_id] | |
| container_name = python_vm['container_name'] | |
| current_workdir = workdir or '/' | |
| for line in run_python_code_streaming(container_name, code, current_workdir): | |
| yield line | |
| finally: | |
| self.delete_vm(python_vm_id) | |
| def execute_lua_streaming(self, vm_id, code, workdir=None): | |
| if vm_id in self.vms: | |
| self._update_last_used(vm_id) | |
| lua_vm_id = self._ensure_lua_vm() | |
| if not lua_vm_id: | |
| yield "Error: Failed to ensure Lua VM is available\n" | |
| return | |
| lua_vm = self.vms[lua_vm_id] | |
| container_name = lua_vm['container_name'] | |
| current_workdir = workdir or '/' | |
| for line in run_lua_code_streaming(container_name, code, current_workdir): | |
| yield line | |
| self._update_last_used(lua_vm_id) | |
| def get_vm_status(self, vm_id): | |
| if vm_id not in self.vms: | |
| return None | |
| self._update_last_used(vm_id) | |
| vm = self.vms[vm_id] | |
| container_name = vm['container_name'] | |
| success, stdout, stderr = run_podman_cmd([ | |
| 'ps', '--filter', f'name={container_name}', '--format', 'json' | |
| ]) | |
| vm_copy = vm.copy() | |
| if success and stdout: | |
| try: | |
| import json as json_lib | |
| containers = json_lib.loads(stdout) | |
| if containers: | |
| vm_copy['status'] = 'running' | |
| else: | |
| vm_copy['status'] = 'stopped' | |
| except: | |
| vm_copy['status'] = 'unknown' | |
| else: | |
| vm_copy['status'] = 'error' | |
| return vm_copy | |
| def get_vm_cwd(self, vm_id): | |
| if vm_id not in self.vms: | |
| return None | |
| self._update_last_used(vm_id) | |
| return self.vms[vm_id]['cwd'] | |
| def list_vms(self): | |
| return list(self.vms.keys()) | |
| def delete_vm(self, vm_id): | |
| if vm_id not in self.vms: | |
| return False | |
| container_name = self.vms[vm_id]['container_name'] | |
| run_podman_cmd(['stop', container_name]) | |
| run_podman_cmd(['rm', container_name]) | |
| del self.vms[vm_id] | |
| return True | |
| manager = VMManager() | |
| def index(): | |
| return """ | |
| <html> | |
| <head><title>Simple VM API</title></head> | |
| <body> | |
| <h1>Simple VM API</h1> | |
| <p>Endpoints:</p> | |
| <ul> | |
| <li>POST /vm - Create VM (supports image and install_python params)</li> | |
| <li>POST /vm/<id>/command - Run command</li> | |
| <li>POST /vm/<id>/python - Execute Python code (streaming)</li> | |
| <li>POST /vm/<id>/lua - Execute Lua code (streaming)</li> | |
| <li>POST /vm/<id>/copy-to - Copy file to VM</li> | |
| <li>POST /vm/<id>/copy-from - Copy file from VM</li> | |
| <li>GET /vm/<id>/status - Get VM status</li> | |
| <li>GET /vm/<id>/cwd - Get current working directory</li> | |
| <li>DELETE /vm/<id> - Delete VM</li> | |
| <li>GET /cleanup - Delete VMs unused for 10+ minutes</li> | |
| <li>GET /poll - Poll for updates</li> | |
| <p><strong>Server runs on port 7860</strong></p> | |
| </ul> | |
| </body> | |
| </html> | |
| """ | |
| def create_vm(): | |
| data = request.get_json() or {} | |
| vcpu = data.get('vcpu', 1) | |
| memory = data.get('memory', '512m') | |
| image = data.get('image', 'debian:latest') | |
| install_python = data.get('install_python', True) | |
| vm_id = manager.create_vm(vcpu, memory, image, install_python) | |
| return jsonify({'vm_id': vm_id, 'vcpu': vcpu, 'memory': memory, 'image': image}) | |
| def run_command(vm_id): | |
| data = request.get_json() or {} | |
| command = data.get('command', '') | |
| result = manager.run_command(vm_id, command) | |
| if result is None: | |
| return jsonify({'error': 'VM not found'}), 404 | |
| return jsonify({'output': result, 'command': command}) | |
| def get_status(vm_id): | |
| status = manager.get_vm_status(vm_id) | |
| if not status: | |
| return jsonify({'error': 'VM not found'}), 404 | |
| return jsonify(status) | |
| def get_cwd(vm_id): | |
| cwd = manager.get_vm_cwd(vm_id) | |
| if cwd is None: | |
| return jsonify({'error': 'VM not found'}), 404 | |
| return jsonify({'cwd': cwd}) | |
| def execute_python(vm_id): | |
| if vm_id in manager.vms: | |
| manager._update_last_used(vm_id) | |
| code = None | |
| if 'file' in request.files and request.files['file'].filename: | |
| file = request.files['file'] | |
| if not file.filename.endswith('.py'): | |
| return jsonify({'error': 'Only .py files are allowed'}), 400 | |
| file.seek(0, os.SEEK_END) | |
| file_size = file.tell() | |
| file.seek(0) | |
| if file_size > 10 * 1024 * 1024: | |
| return jsonify({'error': 'File too large (max 10MB)'}), 400 | |
| code = file.read().decode('utf-8') | |
| elif request.is_json and request.get_json().get('code'): | |
| code = request.get_json()['code'] | |
| else: | |
| return jsonify({'error': 'No Python code provided. Use "code" field in JSON or upload a .py file'}), 400 | |
| if not code or not code.strip(): | |
| return jsonify({'error': 'Empty Python code'}), 400 | |
| workdir = request.args.get('workdir') or request.get_json().get('workdir') if request.is_json else None | |
| def generate(): | |
| try: | |
| for line in manager.execute_python_streaming(vm_id, code, workdir): | |
| yield f"data: {line}\n\n" | |
| except Exception as e: | |
| yield f"data: Error: {str(e)}\n\n" | |
| return app.response_class(generate(), mimetype='text/event-stream') | |
| def execute_lua(vm_id): | |
| if vm_id in manager.vms: | |
| manager._update_last_used(vm_id) | |
| code = None | |
| if 'file' in request.files and request.files['file'].filename: | |
| file = request.files['file'] | |
| if not file.filename.endswith('.lua'): | |
| return jsonify({'error': 'Only .lua files are allowed'}), 400 | |
| file.seek(0, os.SEEK_END) | |
| file_size = file.tell() | |
| file.seek(0) | |
| if file_size > 10 * 1024 * 1024: | |
| return jsonify({'error': 'File too large (max 10MB)'}), 400 | |
| code = file.read().decode('utf-8') | |
| elif request.is_json and request.get_json().get('code'): | |
| code = request.get_json()['code'] | |
| else: | |
| return jsonify({'error': 'No Lua code provided. Use "code" field in JSON or upload a .lua file'}), 400 | |
| if not code or not code.strip(): | |
| return jsonify({'error': 'Empty Lua code'}), 400 | |
| workdir = request.args.get('workdir') or request.get_json().get('workdir') if request.is_json else None | |
| def generate(): | |
| try: | |
| for line in manager.execute_lua_streaming(vm_id, code, workdir): | |
| yield f"data: {line}\n\n" | |
| except Exception as e: | |
| yield f"data: Error: {str(e)}\n\n" | |
| return app.response_class(generate(), mimetype='text/event-stream') | |
| def delete_vm(vm_id): | |
| if manager.delete_vm(vm_id): | |
| return jsonify({'message': 'VM deleted'}) | |
| return jsonify({'error': 'VM not found'}), 404 | |
| def copy_to_vm(vm_id): | |
| data = request.get_json() or {} | |
| local_path = data.get('local_path', '') | |
| container_path = data.get('container_path', '') | |
| if not local_path or not container_path: | |
| return jsonify({'error': 'local_path and container_path required'}), 400 | |
| if not os.path.exists(local_path): | |
| return jsonify({'error': 'Local file does not exist'}), 400 | |
| success = manager.copy_file_to_vm(vm_id, local_path, container_path) | |
| if success: | |
| return jsonify({'message': 'File copied successfully'}) | |
| return jsonify({'error': 'Failed to copy file'}), 500 | |
| def copy_from_vm(vm_id): | |
| data = request.get_json() or {} | |
| container_path = data.get('container_path', '') | |
| local_path = data.get('local_path', '') | |
| if not container_path or not local_path: | |
| return jsonify({'error': 'container_path and local_path required'}), 400 | |
| success = manager.copy_file_from_vm(vm_id, container_path, local_path) | |
| if success: | |
| return jsonify({'message': 'File copied successfully'}) | |
| return jsonify({'error': 'Failed to copy file'}), 500 | |
| def cleanup(): | |
| def cleanup_generator(): | |
| import time | |
| import json as json_lib | |
| if manager._cleanup_running: | |
| yield f"data: {json_lib.dumps({'event': 'busy', 'message': 'Cleanup already running, please wait...'})}\n\n" | |
| return | |
| manager._cleanup_running = True | |
| try: | |
| current_time = time.time() | |
| cleanup_threshold = 600 # 10 minutes for manual cleanup | |
| deleted_count = 0 | |
| orphaned_containers_deleted = 0 | |
| yield f"data: {json_lib.dumps({'event': 'started', 'message': 'Starting manual cleanup process (10+ minute threshold)...'})}\n\n" | |
| vms_info = [] | |
| vms_to_delete = [] | |
| # First, analyze managed VMs | |
| yield f"data: {json_lib.dumps({'event': 'analyzing_vms', 'message': 'Analyzing managed VMs...'})}\n\n" | |
| for vm_id, vm in manager.vms.items(): | |
| last_activity = vm.get('last_used', vm.get('created', 0)) | |
| age_seconds = current_time - last_activity | |
| age_minutes = age_seconds / 60 | |
| vm_info = { | |
| 'vm_id': vm_id, | |
| 'age_minutes': round(age_minutes, 1), | |
| 'is_lua_vm': vm_id == manager.lua_vm_id, | |
| 'should_delete': age_seconds > cleanup_threshold and vm_id != manager.lua_vm_id | |
| } | |
| vms_info.append(vm_info) | |
| if vm_id == manager.lua_vm_id: | |
| continue | |
| if age_seconds > cleanup_threshold: | |
| vms_to_delete.append(vm_id) | |
| yield f"data: {json_lib.dumps({'event': 'vms_analysis_complete', 'total_vms': len(manager.vms), 'idle_vms': len(vms_to_delete), 'vms_info': vms_info})}\n\n" | |
| # Clean up managed VMs that are idle | |
| if vms_to_delete: | |
| yield f"data: {json_lib.dumps({'event': 'deleting_vms', 'message': f'Deleting {len(vms_to_delete)} idle VMs...'})}\n\n" | |
| for vm_id in vms_to_delete: | |
| age_minutes = round((current_time - manager.vms[vm_id].get('last_used', manager.vms[vm_id]['created'])) / 60, 1) | |
| yield f"data: {json_lib.dumps({'event': 'deleting_vm', 'vm_id': vm_id, 'age_minutes': age_minutes})}\n\n" | |
| print(f"Manual cleanup: deleting idle VM {vm_id} (unused for {age_minutes} minutes)") | |
| manager.delete_vm(vm_id) | |
| deleted_count += 1 | |
| yield f"data: {json_lib.dumps({'event': 'vm_deleted', 'vm_id': vm_id, 'deleted_count': deleted_count})}\n\n" | |
| # Now clean up orphaned podman containers | |
| yield f"data: {json_lib.dumps({'event': 'checking_containers', 'message': 'Checking for orphaned podman containers...'})}\n\n" | |
| try: | |
| # Get all containers with our naming pattern | |
| ps_success, ps_stdout, ps_stderr = run_podman_cmd(['ps', '-a', '--filter', 'name=vm-', '--format', 'json']) | |
| if ps_success and ps_stdout: | |
| try: | |
| containers = json_lib.loads(ps_stdout) | |
| orphaned_containers = [] | |
| if isinstance(containers, list): | |
| for container in containers: | |
| container_name = container.get('Names', [''])[0] if container.get('Names') else '' | |
| if container_name.startswith('vm-'): | |
| vm_id = container_name[3:] # Remove 'vm-' prefix | |
| # Check if this VM is still managed | |
| if vm_id not in manager.vms: | |
| orphaned_containers.append(container_name) | |
| if orphaned_containers: | |
| yield f"data: {json_lib.dumps({'event': 'orphaned_found', 'count': len(orphaned_containers), 'containers': orphaned_containers})}\n\n" | |
| for container_name in orphaned_containers: | |
| yield f"data: {json_lib.dumps({'event': 'deleting_container', 'container': container_name})}\n\n" | |
| print(f"Manual cleanup: deleting orphaned container {container_name}") | |
| rm_success, rm_stdout, rm_stderr = run_podman_cmd(['rm', '-f', container_name]) | |
| if rm_success: | |
| orphaned_containers_deleted += 1 | |
| yield f"data: {json_lib.dumps({'event': 'container_deleted', 'container': container_name, 'orphaned_deleted': orphaned_containers_deleted})}\n\n" | |
| else: | |
| yield f"data: {json_lib.dumps({'event': 'container_delete_failed', 'container': container_name, 'error': rm_stderr})}\n\n" | |
| else: | |
| yield f"data: {json_lib.dumps({'event': 'no_orphaned', 'message': 'No orphaned containers found'})}\n\n" | |
| except Exception as e: | |
| yield f"data: {json_lib.dumps({'event': 'error', 'message': f'Error parsing container list: {str(e)}'})}\n\n" | |
| else: | |
| yield f"data: {json_lib.dumps({'event': 'error', 'message': f'Failed to list containers: {ps_stderr}'})}\n\n" | |
| except Exception as e: | |
| yield f"data: {json_lib.dumps({'event': 'error', 'message': f'Error checking for orphaned containers: {str(e)}'})}\n\n" | |
| # Final summary | |
| summary_data = { | |
| 'event': 'completed', | |
| 'summary': { | |
| 'deleted_vms': deleted_count, | |
| 'deleted_containers': orphaned_containers_deleted, | |
| 'total_cleaned': deleted_count + orphaned_containers_deleted, | |
| 'remaining_vms': len(manager.vms) | |
| } | |
| } | |
| yield f"data: {json_lib.dumps(summary_data)}\n\n" | |
| finally: | |
| manager._cleanup_running = False | |
| return app.response_class(cleanup_generator(), mimetype='text/event-stream') | |
| def poll(): | |
| return jsonify({'vms': manager.list_vms()}) | |
| def main(): | |
| if len(sys.argv) < 2: | |
| command = "server" | |
| else: | |
| command = sys.argv[1] | |
| if command == "server": | |
| if not FLASK_AVAILABLE: | |
| print("Flask not available") | |
| return | |
| # Check if podman is available (will auto-install on Linux if needed) | |
| check_podman, podman_version, _ = run_podman_cmd(['--version']) | |
| if check_podman: | |
| print(f"Podman is available - {podman_version.strip()}") | |
| else: | |
| print("Podman not found initially, but will be auto-installed on first use if running on Linux") | |
| print("Starting simple VM API server on http://localhost:7860") | |
| print("Podman is available - ready to create containers!") | |
| app.run(host='0.0.0.0', port=7860, debug=True) | |
| elif command == "list": | |
| if not FLASK_AVAILABLE: | |
| print("Flask not available") | |
| return | |
| manager = VMManager() | |
| vms = manager.list_vms() | |
| print(f"VMs: {vms}") | |
| else: | |
| print("Usage: python3 lib.py [server|list]") | |
| if __name__ == "__main__": | |
| main() |