fix: replace storage.cfg parser with pvesm status for reliable pool discovery; add Custom... dropdown option
This commit is contained in:
parent
e53fcacaa6
commit
2e0c6cdba7
4 changed files with 58 additions and 33 deletions
|
|
@ -97,42 +97,34 @@ def health() -> HealthResponse:
|
||||||
@app.get(f"{API_PREFIX}/storage/pools")
|
@app.get(f"{API_PREFIX}/storage/pools")
|
||||||
def storage_pools():
|
def storage_pools():
|
||||||
"""Return available Proxmox storage pools that support VM disk images."""
|
"""Return available Proxmox storage pools that support VM disk images."""
|
||||||
pools = []
|
result = []
|
||||||
try:
|
try:
|
||||||
cfg = Path("/etc/pve/storage.cfg")
|
import subprocess
|
||||||
if cfg.exists():
|
out = subprocess.run(["pvesm", "status"], capture_output=True, text=True, timeout=10)
|
||||||
lines = cfg.read_text()
|
for line in out.stdout.split("\n")[1:]:
|
||||||
current = {}
|
parts = line.split()
|
||||||
for line in lines.split("\n"):
|
if len(parts) < 3:
|
||||||
stripped = line.strip()
|
continue
|
||||||
if not stripped or stripped.startswith("#"):
|
name, stype, active = parts[0], parts[1], parts[2]
|
||||||
continue
|
if active != "active":
|
||||||
if ":" in stripped and not line.startswith(" "):
|
continue
|
||||||
if current and current.get("name"):
|
if stype not in ("lvmthin", "zfspool", "rbd", "dir", "nfs"):
|
||||||
pools.append(current)
|
continue
|
||||||
parts = stripped.split(":", 1)
|
# Verify content includes images
|
||||||
stype = parts[0].strip()
|
content = ""
|
||||||
sname = parts[1].strip() if len(parts) > 1 else ""
|
try:
|
||||||
current = {"name": sname, "type": stype}
|
cp = subprocess.run(["pvesm", "config", name], capture_output=True, text=True, timeout=5)
|
||||||
elif line.startswith(" ") and current:
|
for cl in cp.stdout.split("\n"):
|
||||||
if " " in stripped:
|
if cl.strip().startswith("content"):
|
||||||
key, val = stripped.split(" ", 1)
|
content = cl.strip().split(None, 1)[1] if len(cl.strip().split(None, 1)) > 1 else ""
|
||||||
current[key] = val
|
except Exception:
|
||||||
if current and current.get("name"):
|
pass
|
||||||
pools.append(current)
|
if "images" not in content:
|
||||||
|
continue
|
||||||
|
result.append({"name": name, "type": stype})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
result = []
|
|
||||||
for p in pools:
|
|
||||||
if p.get("type") in ("lvmthin", "zfspool", "rbd", "dir"):
|
|
||||||
content = p.get("content", "")
|
|
||||||
if "images" in content:
|
|
||||||
result.append({
|
|
||||||
"name": p["name"],
|
|
||||||
"type": p["type"],
|
|
||||||
})
|
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
result.append({"name": "local-lvm", "type": "lvmthin"})
|
result.append({"name": "local-lvm", "type": "lvmthin"})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,14 @@
|
||||||
<div class="pve-form-group">
|
<div class="pve-form-group">
|
||||||
<label>Storage Pool</label>
|
<label>Storage Pool</label>
|
||||||
{% if storage_pools %}
|
{% if storage_pools %}
|
||||||
<select name="target_storage" class="pve-select">
|
<select name="target_storage" class="pve-select" id="storage-select">
|
||||||
{% for pool in storage_pools %}
|
{% for pool in storage_pools %}
|
||||||
<option value="{{ pool.name }}">{{ pool.name }} ({{ pool.type }})</option>
|
<option value="{{ pool.name }}">{{ pool.name }} ({{ pool.type }})</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<option value="__custom__">Custom...</option>
|
||||||
</select>
|
</select>
|
||||||
|
<input type="text" class="pve-input pve-hidden" id="storage-custom"
|
||||||
|
placeholder="Enter pool name" style="margin-top:0.3rem;">
|
||||||
{% else %}
|
{% else %}
|
||||||
<input type="text" name="target_storage" class="pve-input" value="local-lvm">
|
<input type="text" name="target_storage" class="pve-input" value="local-lvm">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,21 @@ function initConfirmForm() {
|
||||||
this.value === 'false' ? 'block' : 'none';
|
this.value === 'false' ? 'block' : 'none';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const storageSel = document.getElementById('storage-select');
|
||||||
|
const storageCustom = document.getElementById('storage-custom');
|
||||||
|
if (storageSel && storageCustom) {
|
||||||
|
storageSel.addEventListener('change', function() {
|
||||||
|
if (this.value === '__custom__') {
|
||||||
|
storageCustom.classList.remove('pve-hidden');
|
||||||
|
storageCustom.name = 'target_storage';
|
||||||
|
this.name = '';
|
||||||
|
} else {
|
||||||
|
storageCustom.classList.add('pve-hidden');
|
||||||
|
storageCustom.name = 'target_storage_custom';
|
||||||
|
this.name = 'target_storage';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitJob(e) {
|
async function submitJob(e) {
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,21 @@ function scpInitConfirmForm() {
|
||||||
this.value === 'false' ? 'block' : 'none';
|
this.value === 'false' ? 'block' : 'none';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const storageSel = document.getElementById('storage-select');
|
||||||
|
const storageCustom = document.getElementById('storage-custom');
|
||||||
|
if (storageSel && storageCustom) {
|
||||||
|
storageSel.addEventListener('change', function() {
|
||||||
|
if (this.value === '__custom__') {
|
||||||
|
storageCustom.classList.remove('pve-hidden');
|
||||||
|
storageCustom.name = 'target_storage';
|
||||||
|
this.name = '';
|
||||||
|
} else {
|
||||||
|
storageCustom.classList.add('pve-hidden');
|
||||||
|
storageCustom.name = 'target_storage_custom';
|
||||||
|
this.name = 'target_storage';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scpSubmitJob(e) {
|
async function scpSubmitJob(e) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue