mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-01 11:56:21 +00:00
update oci manager
This commit is contained in:
@@ -99,7 +99,7 @@ export function SecureGatewaySetup() {
|
|||||||
const loadInitialData = async () => {
|
const loadInitialData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
// Load runtime info
|
// Load runtime info (just for display, deploy will auto-install if needed)
|
||||||
const runtimeRes = await fetchApi("/api/oci/runtime")
|
const runtimeRes = await fetchApi("/api/oci/runtime")
|
||||||
if (runtimeRes.success && runtimeRes.available) {
|
if (runtimeRes.success && runtimeRes.available) {
|
||||||
setRuntimeAvailable(true)
|
setRuntimeAvailable(true)
|
||||||
@@ -602,37 +602,6 @@ export function SecureGatewaySetup() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runtime not available
|
|
||||||
if (!runtimeAvailable) {
|
|
||||||
return (
|
|
||||||
<Card className="border-border bg-card">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ShieldCheck className="h-5 w-5 text-cyan-500" />
|
|
||||||
<CardTitle className="text-base">Secure Gateway</CardTitle>
|
|
||||||
</div>
|
|
||||||
<CardDescription>VPN access without opening ports</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-yellow-500">Container Runtime Required</p>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Install Podman or Docker to use OCI applications.
|
|
||||||
</p>
|
|
||||||
<code className="text-xs mt-2 block bg-muted/50 p-2 rounded">
|
|
||||||
apt install podman
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Installed state
|
// Installed state
|
||||||
if (appStatus.state !== "not_installed") {
|
if (appStatus.state !== "not_installed") {
|
||||||
const isRunning = appStatus.state === "running"
|
const isRunning = appStatus.state === "running"
|
||||||
@@ -828,10 +797,17 @@ export function SecureGatewaySetup() {
|
|||||||
Deploy a Tailscale VPN gateway for secure remote access to your Proxmox infrastructure. No port forwarding required.
|
Deploy a Tailscale VPN gateway for secure remote access to your Proxmox infrastructure. No port forwarding required.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
{runtimeAvailable ? (
|
||||||
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span>{runtimeInfo?.runtime} {runtimeInfo?.version} available</span>
|
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
||||||
</div>
|
<span>{runtimeInfo?.runtime} {runtimeInfo?.version} available</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Info className="h-3.5 w-3.5 text-cyan-500" />
|
||||||
|
<span>Podman will be installed automatically during deployment</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowWizard(true)}
|
onClick={() => setShowWizard(true)}
|
||||||
|
|||||||
@@ -420,6 +420,36 @@ def get_runtime():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@oci_bp.route("/runtime/install-script", methods=["GET"])
|
||||||
|
@require_auth
|
||||||
|
def get_runtime_install_script():
|
||||||
|
"""
|
||||||
|
Get the path to the runtime installation script.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Script path for installing Podman.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Check possible paths for the install script
|
||||||
|
possible_paths = [
|
||||||
|
"/usr/local/share/proxmenux/scripts/oci/install_runtime.sh",
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "Scripts", "oci", "install_runtime.sh"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for script_path in possible_paths:
|
||||||
|
if os.path.exists(script_path):
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"script_path": os.path.abspath(script_path)
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "Runtime installation script not found"
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
|
||||||
@oci_bp.route("/status/<app_id>", methods=["GET"])
|
@oci_bp.route("/status/<app_id>", methods=["GET"])
|
||||||
@require_auth
|
@require_auth
|
||||||
def get_app_status(app_id: str):
|
def get_app_status(app_id: str):
|
||||||
|
|||||||
@@ -1125,40 +1125,6 @@ class HealthMonitor:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Check disk_observations for active (non-dismissed) warnings
|
|
||||||
# This ensures disks with persistent observations appear in Health Monitor
|
|
||||||
# even if the error is not currently in the logs
|
|
||||||
try:
|
|
||||||
all_observations = health_persistence.get_disk_observations()
|
|
||||||
for obs in all_observations:
|
|
||||||
device_name = obs.get('device_name', '').replace('/dev/', '')
|
|
||||||
if not device_name:
|
|
||||||
continue
|
|
||||||
severity = (obs.get('severity') or 'warning').upper()
|
|
||||||
if severity in ('WARNING', 'CRITICAL') and not obs.get('dismissed'):
|
|
||||||
# Add to issues if not already present
|
|
||||||
obs_reason = obs.get('raw_message', f'{device_name}: Disk observation recorded')
|
|
||||||
obs_key = f'/dev/{device_name}'
|
|
||||||
if obs_key not in storage_details:
|
|
||||||
issues.append(obs_reason)
|
|
||||||
storage_details[obs_key] = {
|
|
||||||
'status': severity,
|
|
||||||
'reason': obs_reason,
|
|
||||||
'dismissable': True,
|
|
||||||
}
|
|
||||||
# Ensure disk is in disk_errors_by_device for consolidation
|
|
||||||
if device_name not in disk_errors_by_device:
|
|
||||||
disk_errors_by_device[device_name] = {
|
|
||||||
'status': severity,
|
|
||||||
'reason': obs_reason,
|
|
||||||
'error_type': obs.get('error_type', 'disk_observation'),
|
|
||||||
'serial': obs.get('serial', ''),
|
|
||||||
'model': obs.get('model', ''),
|
|
||||||
'dismissable': True,
|
|
||||||
}
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Build checks dict from storage_details
|
# Build checks dict from storage_details
|
||||||
# We consolidate disk error entries (like /Dev/Sda) into physical disk entries
|
# We consolidate disk error entries (like /Dev/Sda) into physical disk entries
|
||||||
# and only show disks with problems (not healthy ones).
|
# and only show disks with problems (not healthy ones).
|
||||||
@@ -1252,6 +1218,39 @@ class HealthMonitor:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Check disk_observations for active (non-dismissed) warnings
|
||||||
|
# This ensures disks with persistent observations appear in Health Monitor
|
||||||
|
# even if the error is not currently in the logs
|
||||||
|
try:
|
||||||
|
all_observations = health_persistence.get_disk_observations()
|
||||||
|
for obs in all_observations:
|
||||||
|
device_name = obs.get('device_name', '').replace('/dev/', '')
|
||||||
|
if not device_name:
|
||||||
|
continue
|
||||||
|
severity = (obs.get('severity') or 'warning').upper()
|
||||||
|
# Only include if WARNING/CRITICAL and not already dismissed
|
||||||
|
if severity in ('WARNING', 'CRITICAL') and not obs.get('dismissed'):
|
||||||
|
# Check if there's a corresponding acknowledged error in the errors table
|
||||||
|
# If so, skip this observation (it was dismissed via Health Monitor)
|
||||||
|
error_key = f"disk_smart_{device_name}"
|
||||||
|
error_record = health_persistence.get_error_by_key(error_key)
|
||||||
|
if error_record and error_record.get('acknowledged'):
|
||||||
|
continue # Skip - this was dismissed
|
||||||
|
|
||||||
|
# Add to disk_errors_by_device if not already present
|
||||||
|
if device_name not in disk_errors_by_device:
|
||||||
|
obs_reason = obs.get('raw_message', f'{device_name}: Disk observation recorded')
|
||||||
|
disk_errors_by_device[device_name] = {
|
||||||
|
'status': severity,
|
||||||
|
'reason': obs_reason,
|
||||||
|
'error_type': obs.get('error_type', 'disk_observation'),
|
||||||
|
'serial': obs.get('serial', ''),
|
||||||
|
'model': obs.get('model', ''),
|
||||||
|
'dismissable': True,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Add consolidated disk entries (only for disks with errors)
|
# Add consolidated disk entries (only for disks with errors)
|
||||||
for device_name, error_info in disk_errors_by_device.items():
|
for device_name, error_info in disk_errors_by_device.items():
|
||||||
# Try to find this disk in physical_disks for enriched info
|
# Try to find this disk in physical_disks for enriched info
|
||||||
|
|||||||
@@ -70,6 +70,154 @@ def ensure_oci_directories():
|
|||||||
CONTAINER_PREFIX = "proxmenux"
|
CONTAINER_PREFIX = "proxmenux"
|
||||||
|
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Runtime Installation
|
||||||
|
# =================================================================
|
||||||
|
def install_runtime(runtime: str = "podman") -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Install container runtime (podman or docker).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
runtime: Runtime to install ('podman' or 'docker')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with success status and message
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"message": "",
|
||||||
|
"runtime": runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Installing container runtime: {runtime}")
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" Installing {runtime.capitalize()}")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Detect distribution
|
||||||
|
distro = "debian" # Default
|
||||||
|
if os.path.exists("/etc/os-release"):
|
||||||
|
with open("/etc/os-release") as f:
|
||||||
|
content = f.read().lower()
|
||||||
|
if "alpine" in content:
|
||||||
|
distro = "alpine"
|
||||||
|
elif "arch" in content:
|
||||||
|
distro = "arch"
|
||||||
|
elif "fedora" in content or "rhel" in content or "centos" in content:
|
||||||
|
distro = "rhel"
|
||||||
|
|
||||||
|
# Install commands by distro
|
||||||
|
install_commands = {
|
||||||
|
"debian": {
|
||||||
|
"podman": ["apt-get", "update", "&&", "apt-get", "install", "-y", "podman"],
|
||||||
|
"docker": ["apt-get", "update", "&&", "apt-get", "install", "-y", "docker.io"]
|
||||||
|
},
|
||||||
|
"alpine": {
|
||||||
|
"podman": ["apk", "add", "--no-cache", "podman"],
|
||||||
|
"docker": ["apk", "add", "--no-cache", "docker"]
|
||||||
|
},
|
||||||
|
"arch": {
|
||||||
|
"podman": ["pacman", "-Sy", "--noconfirm", "podman"],
|
||||||
|
"docker": ["pacman", "-Sy", "--noconfirm", "docker"]
|
||||||
|
},
|
||||||
|
"rhel": {
|
||||||
|
"podman": ["dnf", "install", "-y", "podman"],
|
||||||
|
"docker": ["dnf", "install", "-y", "docker-ce"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get install command
|
||||||
|
if distro == "debian":
|
||||||
|
# Use shell for && syntax
|
||||||
|
if runtime == "podman":
|
||||||
|
cmd = "apt-get update && apt-get install -y podman"
|
||||||
|
else:
|
||||||
|
cmd = "apt-get update && apt-get install -y docker.io"
|
||||||
|
|
||||||
|
print(f"[*] Running: {cmd}")
|
||||||
|
proc = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
shell=True,
|
||||||
|
capture_output=False,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cmd = install_commands.get(distro, {}).get(runtime, [])
|
||||||
|
if not cmd:
|
||||||
|
result["message"] = f"Unsupported distro: {distro}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
print(f"[*] Running: {' '.join(cmd)}")
|
||||||
|
proc = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=False,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
result["message"] = f"Failed to install {runtime}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Configure podman registries if needed
|
||||||
|
if runtime == "podman":
|
||||||
|
registries_conf = "/etc/containers/registries.conf"
|
||||||
|
if os.path.exists("/etc/containers") and not os.path.exists(registries_conf):
|
||||||
|
try:
|
||||||
|
with open(registries_conf, 'w') as f:
|
||||||
|
f.write('unqualified-search-registries = ["docker.io", "quay.io", "ghcr.io"]\n')
|
||||||
|
print("[*] Configured container registries")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not configure registries: {e}")
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
verify_cmd = shutil.which(runtime)
|
||||||
|
if verify_cmd:
|
||||||
|
print(f"\n[OK] {runtime.capitalize()} installed successfully!")
|
||||||
|
result["success"] = True
|
||||||
|
result["message"] = f"{runtime.capitalize()} installed successfully"
|
||||||
|
result["path"] = verify_cmd
|
||||||
|
else:
|
||||||
|
result["message"] = f"{runtime.capitalize()} installed but not found in PATH"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result["message"] = "Installation timed out"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to install runtime: {e}")
|
||||||
|
result["message"] = str(e)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_runtime() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Ensure a container runtime is available, installing if necessary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with runtime info (same as detect_runtime)
|
||||||
|
"""
|
||||||
|
runtime_info = detect_runtime()
|
||||||
|
|
||||||
|
if runtime_info["available"]:
|
||||||
|
return runtime_info
|
||||||
|
|
||||||
|
# No runtime available, install podman
|
||||||
|
print("\n[!] No container runtime found. Installing Podman...")
|
||||||
|
install_result = install_runtime("podman")
|
||||||
|
|
||||||
|
if not install_result["success"]:
|
||||||
|
return {
|
||||||
|
"available": False,
|
||||||
|
"runtime": None,
|
||||||
|
"version": None,
|
||||||
|
"path": None,
|
||||||
|
"error": install_result["message"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-detect after installation
|
||||||
|
return detect_runtime()
|
||||||
|
|
||||||
|
|
||||||
# =================================================================
|
# =================================================================
|
||||||
# Runtime Detection
|
# Runtime Detection
|
||||||
# =================================================================
|
# =================================================================
|
||||||
@@ -416,10 +564,10 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
|||||||
"app_id": app_id
|
"app_id": app_id
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check runtime
|
# Ensure runtime is available (install if necessary)
|
||||||
runtime_info = detect_runtime()
|
runtime_info = ensure_runtime()
|
||||||
if not runtime_info["available"]:
|
if not runtime_info["available"]:
|
||||||
result["message"] = runtime_info.get("error", "No container runtime available")
|
result["message"] = runtime_info.get("error", "Failed to setup container runtime")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
runtime = runtime_info["runtime"]
|
runtime = runtime_info["runtime"]
|
||||||
|
|||||||
Reference in New Issue
Block a user