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:
parent
878bc66da8
commit
7eb2d6bb45
4 changed files with 65 additions and 2 deletions
|
|
@ -94,6 +94,49 @@ def health() -> HealthResponse:
|
||||||
return HealthResponse(status="ok")
|
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)
|
@app.post(f"{API_PREFIX}/analyze", response_model=AnalyzeStatusResponse, status_code=202)
|
||||||
def analyze(req: AnalyzeRequest) -> AnalyzeStatusResponse:
|
def analyze(req: AnalyzeRequest) -> AnalyzeStatusResponse:
|
||||||
"""Analyze a source file asynchronously: extraction + disk probing + OS detection."""
|
"""Analyze a source file asynchronously: extraction + disk probing + OS detection."""
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ class ApiClient:
|
||||||
"""GET /api/v1/analyze/{analysis_id} — poll analysis status."""
|
"""GET /api/v1/analyze/{analysis_id} — poll analysis status."""
|
||||||
return self._get(f"/api/v1/analyze/{analysis_id}")
|
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:
|
def create_job(self, payload: dict) -> dict:
|
||||||
"""POST /api/v1/jobs — submit conversion job."""
|
"""POST /api/v1/jobs — submit conversion job."""
|
||||||
return self._post("/api/v1/jobs", payload)
|
return self._post("/api/v1/jobs", payload)
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,13 @@ async def session_analyze_result(
|
||||||
return render("_analysis.html", request=request,
|
return render("_analysis.html", request=request,
|
||||||
error=f"Failed to fetch analysis: {exc.detail}")
|
error=f"Failed to fetch analysis: {exc.detail}")
|
||||||
|
|
||||||
|
pools = []
|
||||||
|
try:
|
||||||
|
sp = api.get_storage_pools()
|
||||||
|
pools = sp.get("pools", [])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
analysis = result
|
analysis = result
|
||||||
name = vm_name.strip()
|
name = vm_name.strip()
|
||||||
if not name:
|
if not name:
|
||||||
|
|
@ -363,7 +370,7 @@ async def session_analyze_result(
|
||||||
|
|
||||||
return render("_analysis.html", request=request,
|
return render("_analysis.html", request=request,
|
||||||
vmid=vmid, source_filename=source_filename,
|
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)
|
@app.post("/session/confirm", response_class=HTMLResponse)
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,16 @@
|
||||||
<div class="pve-row">
|
<div class="pve-row">
|
||||||
<div class="pve-form-group">
|
<div class="pve-form-group">
|
||||||
<label>Storage Pool</label>
|
<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>
|
||||||
<div class="pve-form-group">
|
<div class="pve-form-group">
|
||||||
<label>Boot Detection</label>
|
<label>Boot Detection</label>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue