mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-29 10:56:26 +00:00
Update oci_manager.py
This commit is contained in:
@@ -274,7 +274,7 @@ def _get_vmid_for_app(app_id: str) -> Optional[int]:
|
|||||||
def pull_oci_image(image: str, tag: str = "latest", storage: str = DEFAULT_STORAGE) -> Dict[str, Any]:
|
def pull_oci_image(image: str, tag: str = "latest", storage: str = DEFAULT_STORAGE) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Pull an OCI image from a registry and store as LXC template.
|
Pull an OCI image from a registry and store as LXC template.
|
||||||
Uses skopeo to download OCI images (the same method Proxmox GUI uses).
|
Uses Proxmox's pvesh API to download OCI images (same as GUI).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
image: Image name (e.g., "docker.io/tailscale/tailscale")
|
image: Image name (e.g., "docker.io/tailscale/tailscale")
|
||||||
@@ -296,66 +296,81 @@ def pull_oci_image(image: str, tag: str = "latest", storage: str = DEFAULT_STORA
|
|||||||
result["message"] = pve_info.get("error", "OCI not supported")
|
result["message"] = pve_info.get("error", "OCI not supported")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Check skopeo is available
|
# Normalize image name - ensure full registry path
|
||||||
if not shutil.which("skopeo"):
|
|
||||||
result["message"] = "skopeo not found. Please install: apt install skopeo"
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Normalize image name
|
|
||||||
if not image.startswith(("docker.io/", "ghcr.io/", "quay.io/", "registry.")):
|
if not image.startswith(("docker.io/", "ghcr.io/", "quay.io/", "registry.")):
|
||||||
image = f"docker.io/{image}"
|
image = f"docker.io/{image}"
|
||||||
|
|
||||||
|
# For docker.io, library images need explicit library/ prefix
|
||||||
|
parts = image.split("/")
|
||||||
|
if parts[0] == "docker.io" and len(parts) == 2:
|
||||||
|
image = f"docker.io/library/{parts[1]}"
|
||||||
|
|
||||||
full_ref = f"{image}:{tag}"
|
full_ref = f"{image}:{tag}"
|
||||||
|
|
||||||
logger.info(f"Pulling OCI image: {full_ref}")
|
logger.info(f"Pulling OCI image: {full_ref}")
|
||||||
print(f"[*] Pulling OCI image: {full_ref}")
|
print(f"[*] Pulling OCI image: {full_ref}")
|
||||||
|
|
||||||
# Get the template cache directory for the storage
|
# Create a safe filename from the image reference
|
||||||
# Default is /var/lib/vz/template/cache
|
# e.g., docker.io/tailscale/tailscale:stable -> tailscale-tailscale-stable.tar.zst
|
||||||
template_dir = "/var/lib/vz/template/cache"
|
filename = image.replace("docker.io/", "").replace("ghcr.io/", "").replace("/", "-")
|
||||||
|
filename = f"{filename}-{tag}.tar.zst"
|
||||||
|
|
||||||
# Try to get correct path from storage config
|
# Get hostname for API
|
||||||
rc, out, _ = _run_pve_cmd(["pvesm", "path", f"{storage}:vztmpl/test"])
|
hostname = os.uname().nodename
|
||||||
if rc == 0 and out.strip():
|
|
||||||
# Extract directory from path
|
|
||||||
template_dir = os.path.dirname(out.strip())
|
|
||||||
|
|
||||||
# Create filename from image reference
|
# Use Proxmox's pvesh API to download the OCI image
|
||||||
# Format: registry-name-tag.tar (similar to what Proxmox does)
|
# This is exactly what the GUI does
|
||||||
filename = full_ref.replace("docker.io/", "").replace("/", "-").replace(":", "-") + ".tar"
|
rc, out, err = _run_pve_cmd([
|
||||||
template_path = os.path.join(template_dir, filename)
|
"pvesh", "create",
|
||||||
|
f"/nodes/{hostname}/storage/{storage}/download-url",
|
||||||
|
"--content", "vztmpl",
|
||||||
|
"--filename", filename,
|
||||||
|
"--url", f"docker://{full_ref}"
|
||||||
|
], timeout=600)
|
||||||
|
|
||||||
# Use skopeo to download the image
|
if rc != 0:
|
||||||
# This is exactly what Proxmox does internally
|
# Fallback: try direct skopeo if pvesh API fails
|
||||||
try:
|
logger.warning(f"pvesh download failed: {err}, trying skopeo fallback")
|
||||||
proc = subprocess.run(
|
|
||||||
["skopeo", "copy", f"docker://{full_ref}", f"oci-archive:{template_path}"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=600 # 10 minutes timeout for large images
|
|
||||||
)
|
|
||||||
|
|
||||||
if proc.returncode != 0:
|
if not shutil.which("skopeo"):
|
||||||
result["message"] = f"Failed to pull image: {proc.stderr}"
|
result["message"] = f"Failed to pull image via API: {err}"
|
||||||
logger.error(f"skopeo copy failed: {proc.stderr}")
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# Get template directory
|
||||||
|
template_dir = "/var/lib/vz/template/cache"
|
||||||
|
rc2, out2, _ = _run_pve_cmd(["pvesm", "path", f"{storage}:vztmpl/test"])
|
||||||
|
if rc2 == 0 and out2.strip():
|
||||||
|
template_dir = os.path.dirname(out2.strip())
|
||||||
|
|
||||||
|
template_path = os.path.join(template_dir, filename.replace(".zst", ""))
|
||||||
|
|
||||||
|
# Use skopeo with docker-archive format
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
["skopeo", "copy", "--override-os", "linux",
|
||||||
|
f"docker://{full_ref}", f"docker-archive:{template_path}"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=600
|
||||||
|
)
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
if proc.returncode != 0:
|
||||||
result["message"] = "Image pull timed out after 10 minutes"
|
result["message"] = f"Failed to pull image: {proc.stderr}"
|
||||||
return result
|
logger.error(f"skopeo copy failed: {proc.stderr}")
|
||||||
except Exception as e:
|
return result
|
||||||
result["message"] = f"Failed to pull image: {e}"
|
|
||||||
logger.error(f"Pull failed: {e}")
|
filename = filename.replace(".zst", "")
|
||||||
return result
|
except subprocess.TimeoutExpired:
|
||||||
|
result["message"] = "Image pull timed out after 10 minutes"
|
||||||
# Verify the template was created
|
return result
|
||||||
if not os.path.exists(template_path):
|
except Exception as e:
|
||||||
result["message"] = "Image downloaded but template file not found"
|
result["message"] = f"Failed to pull image: {e}"
|
||||||
return result
|
logger.error(f"Pull failed: {e}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Template was created via API or skopeo
|
||||||
result["success"] = True
|
result["success"] = True
|
||||||
result["template"] = f"{storage}:vztmpl/{filename}"
|
result["template"] = f"{storage}:vztmpl/{filename}"
|
||||||
result["template_path"] = template_path
|
|
||||||
result["message"] = "Image pulled successfully"
|
result["message"] = "Image pulled successfully"
|
||||||
print(f"[OK] Image pulled: {result['template']}")
|
print(f"[OK] Image pulled: {result['template']}")
|
||||||
|
|
||||||
@@ -589,28 +604,22 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
|||||||
# Step 2: Create LXC container
|
# Step 2: Create LXC container
|
||||||
print(f"[*] Creating LXC container...")
|
print(f"[*] Creating LXC container...")
|
||||||
|
|
||||||
# Build pct create command
|
# Build pct create command for OCI container
|
||||||
|
# Note: OCI containers in Proxmox 9.1 have specific requirements
|
||||||
|
# - ostype must be "unmanaged" for OCI images
|
||||||
|
# - features like nesting may not be compatible with all OCI images
|
||||||
pct_cmd = [
|
pct_cmd = [
|
||||||
"pct", "create", str(vmid), template,
|
"pct", "create", str(vmid), template,
|
||||||
"--hostname", hostname,
|
"--hostname", hostname,
|
||||||
"--memory", str(container_def.get("memory", 512)),
|
"--memory", str(container_def.get("memory", 512)),
|
||||||
"--cores", str(container_def.get("cores", 1)),
|
"--cores", str(container_def.get("cores", 1)),
|
||||||
"--rootfs", f"local-lvm:{container_def.get('disk_size', 4)}",
|
"--rootfs", f"local-lvm:{container_def.get('disk_size', 4)}",
|
||||||
"--ostype", "unmanaged",
|
|
||||||
"--unprivileged", "0" if container_def.get("privileged") else "1",
|
"--unprivileged", "0" if container_def.get("privileged") else "1",
|
||||||
"--features", "nesting=1",
|
|
||||||
"--onboot", "1"
|
"--onboot", "1"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Network configuration
|
# Network configuration - use simple bridge with DHCP
|
||||||
net_config = "name=eth0"
|
pct_cmd.extend(["--net0", "name=eth0,bridge=vmbr0,ip=dhcp"])
|
||||||
if container_def.get("network_mode") == "host":
|
|
||||||
# For host network, use bridge with DHCP
|
|
||||||
net_config += ",bridge=vmbr0,ip=dhcp"
|
|
||||||
else:
|
|
||||||
net_config += ",bridge=vmbr0,ip=dhcp"
|
|
||||||
|
|
||||||
pct_cmd.extend(["--net0", net_config])
|
|
||||||
|
|
||||||
# Run pct create
|
# Run pct create
|
||||||
rc, out, err = _run_pve_cmd(pct_cmd, timeout=120)
|
rc, out, err = _run_pve_cmd(pct_cmd, timeout=120)
|
||||||
@@ -620,7 +629,20 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
|||||||
logger.error(f"pct create failed: {err}")
|
logger.error(f"pct create failed: {err}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Step 3: Configure environment variables
|
# Step 3: Apply extra LXC configuration (for unprivileged containers)
|
||||||
|
lxc_config = container_def.get("lxc_config", [])
|
||||||
|
if lxc_config:
|
||||||
|
conf_file = f"/etc/pve/lxc/{vmid}.conf"
|
||||||
|
try:
|
||||||
|
with open(conf_file, 'a') as f:
|
||||||
|
f.write("\n# ProxMenux OCI extra config\n")
|
||||||
|
for config_line in lxc_config:
|
||||||
|
f.write(f"{config_line}\n")
|
||||||
|
logger.info(f"Applied extra LXC config to {conf_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not apply extra LXC config: {e}")
|
||||||
|
|
||||||
|
# Step 4: Configure environment variables
|
||||||
env_vars = []
|
env_vars = []
|
||||||
|
|
||||||
# Add static env vars from container definition
|
# Add static env vars from container definition
|
||||||
@@ -642,11 +664,11 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
|||||||
for i, env in enumerate(env_vars):
|
for i, env in enumerate(env_vars):
|
||||||
_run_pve_cmd(["pct", "set", str(vmid), f"--lxc.environment", env])
|
_run_pve_cmd(["pct", "set", str(vmid), f"--lxc.environment", env])
|
||||||
|
|
||||||
# Step 4: Enable IP forwarding if needed (for VPN containers)
|
# Step 5: Enable IP forwarding if needed (for VPN containers)
|
||||||
if "tailscale" in image.lower() or container_def.get("requires_ip_forward"):
|
if "tailscale" in image.lower() or container_def.get("requires_ip_forward"):
|
||||||
_enable_host_ip_forwarding()
|
_enable_host_ip_forwarding()
|
||||||
|
|
||||||
# Step 5: Start the container
|
# Step 6: Start the container
|
||||||
print(f"[*] Starting container...")
|
print(f"[*] Starting container...")
|
||||||
rc, _, err = _run_pve_cmd(["pct", "start", str(vmid)])
|
rc, _, err = _run_pve_cmd(["pct", "start", str(vmid)])
|
||||||
|
|
||||||
@@ -887,6 +909,64 @@ def detect_host_networks() -> List[Dict[str, Any]]:
|
|||||||
return networks
|
return networks
|
||||||
|
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Network Detection
|
||||||
|
# =================================================================
|
||||||
|
def detect_networks() -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Detect available network interfaces and their subnets.
|
||||||
|
Used for suggesting routes to advertise via Tailscale.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with interface name and subnet.
|
||||||
|
"""
|
||||||
|
networks = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use ip command to get interfaces and their addresses
|
||||||
|
proc = subprocess.run(
|
||||||
|
["ip", "-j", "addr", "show"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if proc.returncode == 0:
|
||||||
|
interfaces = json.loads(proc.stdout)
|
||||||
|
|
||||||
|
for iface in interfaces:
|
||||||
|
name = iface.get("ifname", "")
|
||||||
|
|
||||||
|
# Skip loopback and virtual interfaces
|
||||||
|
if name in ("lo", "docker0") or name.startswith(("veth", "br-", "tap", "fwbr", "fwpr")):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get IPv4 addresses
|
||||||
|
for addr_info in iface.get("addr_info", []):
|
||||||
|
if addr_info.get("family") == "inet":
|
||||||
|
ip = addr_info.get("local", "")
|
||||||
|
prefix = addr_info.get("prefixlen", 24)
|
||||||
|
if ip and not ip.startswith("127."):
|
||||||
|
# Calculate network address
|
||||||
|
ip_parts = ip.split(".")
|
||||||
|
if len(ip_parts) == 4:
|
||||||
|
# Simple network calculation
|
||||||
|
if prefix >= 24:
|
||||||
|
network = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.0/{prefix}"
|
||||||
|
elif prefix >= 16:
|
||||||
|
network = f"{ip_parts[0]}.{ip_parts[1]}.0.0/{prefix}"
|
||||||
|
else:
|
||||||
|
network = f"{ip_parts[0]}.0.0.0/{prefix}"
|
||||||
|
networks.append({
|
||||||
|
"interface": name,
|
||||||
|
"subnet": network,
|
||||||
|
"ip": ip
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to detect networks: {e}")
|
||||||
|
|
||||||
|
return networks
|
||||||
|
|
||||||
|
|
||||||
# =================================================================
|
# =================================================================
|
||||||
# Runtime Detection (for backward compatibility)
|
# Runtime Detection (for backward compatibility)
|
||||||
# =================================================================
|
# =================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user