ai / app.py
superchatai's picture
Update app.py
37165e5 verified
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()
@app.route('/')
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/&lt;id&gt;/command - Run command</li>
<li>POST /vm/&lt;id&gt;/python - Execute Python code (streaming)</li>
<li>POST /vm/&lt;id&gt;/lua - Execute Lua code (streaming)</li>
<li>POST /vm/&lt;id&gt;/copy-to - Copy file to VM</li>
<li>POST /vm/&lt;id&gt;/copy-from - Copy file from VM</li>
<li>GET /vm/&lt;id&gt;/status - Get VM status</li>
<li>GET /vm/&lt;id&gt;/cwd - Get current working directory</li>
<li>DELETE /vm/&lt;id&gt; - 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>
"""
@app.route('/vm', methods=['POST'])
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})
@app.route('/vm/<vm_id>/command', methods=['POST'])
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})
@app.route('/vm/<vm_id>/status')
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)
@app.route('/vm/<vm_id>/cwd')
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})
@app.route('/vm/<vm_id>/python', methods=['POST'])
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')
@app.route('/vm/<vm_id>/lua', methods=['POST'])
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')
@app.route('/vm/<vm_id>', methods=['DELETE'])
def delete_vm(vm_id):
if manager.delete_vm(vm_id):
return jsonify({'message': 'VM deleted'})
return jsonify({'error': 'VM not found'}), 404
@app.route('/vm/<vm_id>/copy-to', methods=['POST'])
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
@app.route('/vm/<vm_id>/copy-from', methods=['POST'])
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
@app.route('/cleanup')
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')
@app.route('/poll')
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()