mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 00:46:21 +00:00
Update oci manager
This commit is contained in:
@@ -30,6 +30,17 @@ interface NetworkInfo {
|
|||||||
recommended?: boolean
|
recommended?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface StorageInfo {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
total: number
|
||||||
|
used: number
|
||||||
|
avail: number
|
||||||
|
active: boolean
|
||||||
|
enabled: boolean
|
||||||
|
recommended: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface AppStatus {
|
interface AppStatus {
|
||||||
state: "not_installed" | "running" | "stopped" | "error"
|
state: "not_installed" | "running" | "stopped" | "error"
|
||||||
health: string
|
health: string
|
||||||
@@ -73,6 +84,7 @@ export function SecureGatewaySetup() {
|
|||||||
const [configSchema, setConfigSchema] = useState<ConfigSchema | null>(null)
|
const [configSchema, setConfigSchema] = useState<ConfigSchema | null>(null)
|
||||||
const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([])
|
const [wizardSteps, setWizardSteps] = useState<WizardStep[]>([])
|
||||||
const [networks, setNetworks] = useState<NetworkInfo[]>([])
|
const [networks, setNetworks] = useState<NetworkInfo[]>([])
|
||||||
|
const [storages, setStorages] = useState<StorageInfo[]>([])
|
||||||
|
|
||||||
// Wizard state
|
// Wizard state
|
||||||
const [showWizard, setShowWizard] = useState(false)
|
const [showWizard, setShowWizard] = useState(false)
|
||||||
@@ -113,14 +125,14 @@ export function SecureGatewaySetup() {
|
|||||||
const loadInitialData = async () => {
|
const loadInitialData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
// Load runtime info (checks for Proxmox 9.1+ OCI support)
|
// Secure Gateway uses standard LXC, not OCI containers
|
||||||
|
// So we don't require PVE 9.1+ - it works on any Proxmox version
|
||||||
|
setRuntimeAvailable(true)
|
||||||
|
|
||||||
|
// Still load runtime info for reference
|
||||||
const runtimeRes = await fetchApi("/api/oci/runtime")
|
const runtimeRes = await fetchApi("/api/oci/runtime")
|
||||||
if (runtimeRes.success && runtimeRes.available) {
|
if (runtimeRes.success) {
|
||||||
setRuntimeAvailable(true)
|
setRuntimeInfo({ runtime: runtimeRes.runtime || "proxmox-lxc", version: runtimeRes.version || "unknown" })
|
||||||
setRuntimeInfo({ runtime: runtimeRes.runtime, version: runtimeRes.version })
|
|
||||||
} else {
|
|
||||||
// Show version requirement message
|
|
||||||
setRuntimeInfo({ runtime: "proxmox-lxc", version: runtimeRes.version || "unknown" })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load app definition
|
// Load app definition
|
||||||
@@ -156,6 +168,17 @@ export function SecureGatewaySetup() {
|
|||||||
setHostIp(ip)
|
setHostIp(ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load available storages
|
||||||
|
const storagesRes = await fetchApi("/api/oci/storages")
|
||||||
|
if (storagesRes.success && storagesRes.storages?.length > 0) {
|
||||||
|
setStorages(storagesRes.storages)
|
||||||
|
// Set default storage (first recommended one)
|
||||||
|
const recommended = storagesRes.storages.find((s: StorageInfo) => s.recommended) || storagesRes.storages[0]
|
||||||
|
if (recommended) {
|
||||||
|
setConfig(prev => ({ ...prev, storage: recommended.name }))
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load data:", err)
|
console.error("Failed to load data:", err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -644,6 +667,51 @@ export function SecureGatewaySetup() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Storage selector */}
|
||||||
|
{storages.length > 1 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-sm font-medium">Storage Location</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">Select where to create the container disk.</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{storages.filter(s => s.active && s.enabled).map((storage) => (
|
||||||
|
<div
|
||||||
|
key={storage.name}
|
||||||
|
onClick={() => setConfig({ ...config, storage: storage.name })}
|
||||||
|
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||||
|
config.storage === storage.name
|
||||||
|
? "border-cyan-500 bg-cyan-500/10"
|
||||||
|
: "border-border hover:border-muted-foreground/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
|
||||||
|
config.storage === storage.name ? "border-cyan-500" : "border-muted-foreground"
|
||||||
|
}`}>
|
||||||
|
{config.storage === storage.name && (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-cyan-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-sm">{storage.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">({storage.type})</span>
|
||||||
|
{storage.recommended && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-green-500/10 text-green-500">
|
||||||
|
Recommended
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{(storage.avail / 1024 / 1024 / 1024).toFixed(1)} GB available
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="bg-muted/30 rounded-lg p-4 space-y-3">
|
<div className="bg-muted/30 rounded-lg p-4 space-y-3">
|
||||||
<h4 className="text-sm font-medium">Configuration Summary</h4>
|
<h4 className="text-sm font-medium">Configuration Summary</h4>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
@@ -651,6 +719,12 @@ export function SecureGatewaySetup() {
|
|||||||
<span className="text-muted-foreground">Hostname:</span>
|
<span className="text-muted-foreground">Hostname:</span>
|
||||||
<span className="font-mono">{config.hostname || "proxmox-gateway"}</span>
|
<span className="font-mono">{config.hostname || "proxmox-gateway"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{storages.length > 1 && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Storage:</span>
|
||||||
|
<span className="font-mono">{config.storage || storages[0]?.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Access Mode:</span>
|
<span className="text-muted-foreground">Access Mode:</span>
|
||||||
<span>{config.access_mode === "host_only" ? "Host Only" : config.access_mode === "proxmox_network" ? "Proxmox Network" : "Custom Networks"}</span>
|
<span>{config.access_mode === "host_only" ? "Host Only" : config.access_mode === "proxmox_network" ? "Proxmox Network" : "Custom Networks"}</span>
|
||||||
@@ -1109,23 +1183,10 @@ export function SecureGatewaySetup() {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
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>
|
||||||
|
|
||||||
{runtimeAvailable ? (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
|
||||||
<span>Proxmox VE {runtimeInfo?.version} - OCI support available</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-yellow-500">
|
|
||||||
<AlertTriangle className="h-3.5 w-3.5" />
|
|
||||||
<span>Requires Proxmox VE 9.1+ (current: {runtimeInfo?.version || "unknown"})</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowWizard(true)}
|
onClick={() => setShowWizard(true)}
|
||||||
className="bg-cyan-600 hover:bg-cyan-700"
|
className="bg-cyan-600 hover:bg-cyan-700"
|
||||||
disabled={!runtimeAvailable}
|
|
||||||
>
|
>
|
||||||
<ShieldCheck className="h-4 w-4 mr-2" />
|
<ShieldCheck className="h-4 w-4 mr-2" />
|
||||||
Deploy Secure Gateway
|
Deploy Secure Gateway
|
||||||
|
|||||||
@@ -80,6 +80,29 @@ def get_app_definition(app_id: str):
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@oci_bp.route("/storages", methods=["GET"])
|
||||||
|
@require_auth
|
||||||
|
def get_storages():
|
||||||
|
"""
|
||||||
|
Get list of available storages for LXC rootfs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of storages with capacity info and recommendations.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
storages = oci_manager.get_available_storages()
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"storages": storages
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get storages: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
@oci_bp.route("/catalog/<app_id>/schema", methods=["GET"])
|
@oci_bp.route("/catalog/<app_id>/schema", methods=["GET"])
|
||||||
@require_auth
|
@require_auth
|
||||||
def get_app_schema(app_id: str):
|
def get_app_schema(app_id: str):
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ ENCRYPTION_KEY_FILE = os.path.join(OCI_BASE_DIR, ".encryption_key")
|
|||||||
# Default storage for templates
|
# Default storage for templates
|
||||||
DEFAULT_STORAGE = "local"
|
DEFAULT_STORAGE = "local"
|
||||||
|
|
||||||
|
# Default storage for rootfs (will be auto-detected)
|
||||||
|
DEFAULT_ROOTFS_STORAGE = "local-lvm"
|
||||||
|
|
||||||
# VMID range for OCI containers (9000-9999 to avoid conflicts)
|
# VMID range for OCI containers (9000-9999 to avoid conflicts)
|
||||||
OCI_VMID_START = 9000
|
OCI_VMID_START = 9000
|
||||||
OCI_VMID_END = 9999
|
OCI_VMID_END = 9999
|
||||||
@@ -259,6 +262,87 @@ def _get_vmid_for_app(app_id: str) -> Optional[int]:
|
|||||||
return instance.get("vmid") if instance else None
|
return instance.get("vmid") if instance else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_storages() -> List[Dict[str, Any]]:
|
||||||
|
"""Get list of storages available for LXC rootfs.
|
||||||
|
|
||||||
|
Returns storages that support 'images' or 'rootdir' content types,
|
||||||
|
which are required for LXC container disks.
|
||||||
|
"""
|
||||||
|
storages = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use pvesm status to get all storages
|
||||||
|
rc, out, err = _run_pve_cmd(["pvesm", "status", "--output-format", "json"])
|
||||||
|
if rc != 0:
|
||||||
|
logger.error(f"Failed to get storage status: {err}")
|
||||||
|
return storages
|
||||||
|
|
||||||
|
storage_list = json.loads(out) if out.strip() else []
|
||||||
|
|
||||||
|
for storage in storage_list:
|
||||||
|
storage_name = storage.get("storage", "")
|
||||||
|
storage_type = storage.get("type", "")
|
||||||
|
|
||||||
|
# Skip if storage is not active
|
||||||
|
if storage.get("active", 0) != 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if storage supports rootdir content (for LXC)
|
||||||
|
rc2, out2, _ = _run_pve_cmd(["pvesm", "show", storage_name, "--output-format", "json"])
|
||||||
|
if rc2 == 0 and out2.strip():
|
||||||
|
try:
|
||||||
|
config = json.loads(out2)
|
||||||
|
content = config.get("content", "")
|
||||||
|
|
||||||
|
# Storage must support "images" or "rootdir" for LXC rootfs
|
||||||
|
# - "rootdir" is for directory-based storage (ZFS, BTRFS, NFS, etc.)
|
||||||
|
# - "images" is for block storage (LVM, LVM-thin, etc.)
|
||||||
|
if "images" in content or "rootdir" in content:
|
||||||
|
total_bytes = storage.get("total", 0)
|
||||||
|
used_bytes = storage.get("used", 0)
|
||||||
|
avail_bytes = storage.get("avail", total_bytes - used_bytes)
|
||||||
|
|
||||||
|
# Determine recommendation priority
|
||||||
|
# Prefer: zfspool > lvmthin > btrfs > lvm > others
|
||||||
|
priority = 99
|
||||||
|
if storage_type == "zfspool":
|
||||||
|
priority = 1
|
||||||
|
elif storage_type == "lvmthin":
|
||||||
|
priority = 2
|
||||||
|
elif storage_type == "btrfs":
|
||||||
|
priority = 3
|
||||||
|
elif storage_type == "lvm":
|
||||||
|
priority = 4
|
||||||
|
elif storage_type in ("dir", "nfs", "cifs"):
|
||||||
|
priority = 10
|
||||||
|
|
||||||
|
storages.append({
|
||||||
|
"name": storage_name,
|
||||||
|
"type": storage_type,
|
||||||
|
"total": total_bytes,
|
||||||
|
"used": used_bytes,
|
||||||
|
"avail": avail_bytes,
|
||||||
|
"active": True,
|
||||||
|
"enabled": storage.get("enabled", 0) == 1,
|
||||||
|
"priority": priority,
|
||||||
|
"recommended": False # Will be set after sorting
|
||||||
|
})
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Sort by priority (lower = better), then by available space
|
||||||
|
storages.sort(key=lambda s: (s.get("priority", 99), -s.get("avail", 0)))
|
||||||
|
|
||||||
|
# Mark the first one as recommended
|
||||||
|
if storages:
|
||||||
|
storages[0]["recommended"] = True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get available storages: {e}")
|
||||||
|
|
||||||
|
return storages
|
||||||
|
|
||||||
|
|
||||||
def _download_alpine_template(storage: str = DEFAULT_STORAGE) -> bool:
|
def _download_alpine_template(storage: str = DEFAULT_STORAGE) -> bool:
|
||||||
"""Download the latest Alpine LXC template using pveam."""
|
"""Download the latest Alpine LXC template using pveam."""
|
||||||
print("[*] Downloading Alpine Linux template...")
|
print("[*] Downloading Alpine Linux template...")
|
||||||
@@ -690,7 +774,7 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
app_id: ID of the app from the catalog
|
app_id: ID of the app from the catalog
|
||||||
config: User configuration values
|
config: User configuration values (includes 'storage' for rootfs location)
|
||||||
installed_by: Source of installation ('web' or 'cli')
|
installed_by: Source of installation ('web' or 'cli')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -703,11 +787,8 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
|||||||
"vmid": None
|
"vmid": None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check Proxmox OCI support
|
# Note: We don't require PVE 9.1+ here because secure-gateway uses standard LXC,
|
||||||
pve_info = check_proxmox_version()
|
# not OCI containers. OCI support will be checked when deploying actual OCI apps.
|
||||||
if not pve_info["oci_support"]:
|
|
||||||
result["message"] = pve_info.get("error", "OCI containers require Proxmox VE 9.1+")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Get app definition
|
# Get app definition
|
||||||
app_def = get_app_definition(app_id)
|
app_def = get_app_definition(app_id)
|
||||||
@@ -723,6 +804,19 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
|||||||
container_def = app_def.get("container", {})
|
container_def = app_def.get("container", {})
|
||||||
container_type = container_def.get("type", "oci")
|
container_type = container_def.get("type", "oci")
|
||||||
|
|
||||||
|
# Get storage for rootfs - from config or auto-detect
|
||||||
|
rootfs_storage = config.get("storage")
|
||||||
|
if not rootfs_storage:
|
||||||
|
# Auto-detect available storage
|
||||||
|
available = get_available_storages()
|
||||||
|
if available:
|
||||||
|
# Use the first one (should be recommended)
|
||||||
|
rootfs_storage = available[0]["name"]
|
||||||
|
logger.info(f"Auto-detected rootfs storage: {rootfs_storage}")
|
||||||
|
else:
|
||||||
|
rootfs_storage = DEFAULT_ROOTFS_STORAGE
|
||||||
|
logger.warning(f"No storage detected, using default: {rootfs_storage}")
|
||||||
|
|
||||||
# Get next available VMID
|
# Get next available VMID
|
||||||
vmid = _get_next_vmid()
|
vmid = _get_next_vmid()
|
||||||
result["vmid"] = vmid
|
result["vmid"] = vmid
|
||||||
@@ -773,7 +867,7 @@ def deploy_app(app_id: str, config: Dict[str, Any], installed_by: str = "web") -
|
|||||||
"--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"{rootfs_storage}:{container_def.get('disk_size', 4)}",
|
||||||
"--unprivileged", "0" if container_def.get("privileged") else "1",
|
"--unprivileged", "0" if container_def.get("privileged") else "1",
|
||||||
"--onboot", "1"
|
"--onboot", "1"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user