feat: storage pool dropdown populated from /etc/pve/storage.cfg

- New GET /api/v1/storage/pools returns available lvmthin/zfspool/rbd/dir pools
- Analysis result template now shows dropdown when >1 pool available
- Falls back to text input with single pool or on discovery failure
This commit is contained in:
Claus Lohmar 2026-07-27 06:55:10 +00:00
parent 878bc66da8
commit 7eb2d6bb45
4 changed files with 65 additions and 2 deletions

View file

@ -94,6 +94,49 @@ def health() -> HealthResponse:
return HealthResponse(status="ok")
@app.get(f"{API_PREFIX}/storage/pools")
def storage_pools():
"""Return available Proxmox storage pools that support disk images."""
pools = []
try:
cfg = Path("/etc/pve/storage.cfg")
if cfg.exists():
lines = cfg.read_text()
current = {}
for line in lines.split("\n"):
line = line.strip()
if not line or line.startswith("#"):
continue
if ":" in line and not line.startswith(" "):
if current and current.get("name"):
pools.append(current)
parts = line.split(":", 1)
stype = parts[0].strip()
sname = parts[1].strip() if len(parts) > 1 else ""
current = {"name": sname, "type": stype}
elif line.startswith(" ") and current:
if " " in line:
key, val = line.strip().split(" ", 1)
current[key] = val
if current and current.get("name"):
pools.append(current)
except Exception:
pass
result = []
for p in pools:
if p.get("type") in ("lvmthin", "zfspool", "rbd", "dir"):
result.append({
"name": p["name"],
"type": p["type"],
})
if not result:
result.append({"name": "local-lvm", "type": "lvmthin"})
return {"pools": result}
@app.post(f"{API_PREFIX}/analyze", response_model=AnalyzeStatusResponse, status_code=202)
def analyze(req: AnalyzeRequest) -> AnalyzeStatusResponse:
"""Analyze a source file asynchronously: extraction + disk probing + OS detection."""

View file

@ -54,6 +54,10 @@ class ApiClient:
"""GET /api/v1/analyze/{analysis_id} — poll analysis status."""
return self._get(f"/api/v1/analyze/{analysis_id}")
def get_storage_pools(self) -> dict:
"""GET /api/v1/storage/pools — list available Proxmox storage pools."""
return self._get("/api/v1/storage/pools")
def create_job(self, payload: dict) -> dict:
"""POST /api/v1/jobs — submit conversion job."""
return self._post("/api/v1/jobs", payload)

View file

@ -356,6 +356,13 @@ async def session_analyze_result(
return render("_analysis.html", request=request,
error=f"Failed to fetch analysis: {exc.detail}")
pools = []
try:
sp = api.get_storage_pools()
pools = sp.get("pools", [])
except Exception:
pass
analysis = result
name = vm_name.strip()
if not name:
@ -363,7 +370,7 @@ async def session_analyze_result(
return render("_analysis.html", request=request,
vmid=vmid, source_filename=source_filename,
vm_name=name, analysis=analysis)
vm_name=name, analysis=analysis, storage_pools=pools)
@app.post("/session/confirm", response_class=HTMLResponse)

View file

@ -63,7 +63,16 @@
<div class="pve-row">
<div class="pve-form-group">
<label>Storage Pool</label>
<input type="text" name="target_storage" class="pve-input" value="local-lvm">
{% if storage_pools and storage_pools|length > 1 %}
<select name="target_storage" class="pve-select">
{% for pool in storage_pools %}
<option value="{{ pool.name }}">{{ pool.name }} ({{ pool.type }})</option>
{% endfor %}
</select>
{% else %}
<input type="text" name="target_storage" class="pve-input"
value="{{ storage_pools[0].name if storage_pools else 'local-lvm' }}">
{% endif %}
</div>
<div class="pve-form-group">
<label>Boot Detection</label>