Detect unsupported quota filesystems (ZFS) and hide quota UI
- _quota_supported() checks df -T /srv; returns false + reason for ZFS, tmpfs, overlay — surfaces as quota_supported/quota_reason in dashboard API - Dashboard quota section shows the reason string instead of an empty table - Quota button hidden from share rows when quota_supported is false - Prevents setquota errors for ZFS-backed Proxmox LXC containers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4943b6b6d
commit
af315b37b2
3 files changed
+29
-6
No files matched your search
@@ -613,6 +613,20 @@ def _shares_with_usage():
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _quota_supported():
|
||||||
|
"""Return (supported: bool, reason: str). Checks filesystem type under /srv."""
|
||||||
|
r = subprocess.run(['df', '-T', '/srv'], capture_output=True, text=True)
|
||||||
|
for line in r.stdout.splitlines()[1:]:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
fstype = parts[1].lower()
|
||||||
|
if fstype == 'zfs':
|
||||||
|
return False, 'ZFS storage detected — Linux disk quotas are not available inside a ZFS-backed container. Manage per-dataset quotas from the Proxmox host with: zfs set quota=10G <dataset>'
|
||||||
|
if fstype in ('tmpfs', 'overlay', 'aufs'):
|
||||||
|
return False, f'{fstype} filesystem does not support disk quotas'
|
||||||
|
return True, ''
|
||||||
|
|
||||||
|
|
||||||
def _quota_usage():
|
def _quota_usage():
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(['repquota', '-u', '/srv'], capture_output=True, text=True, timeout=10)
|
r = subprocess.run(['repquota', '-u', '/srv'], capture_output=True, text=True, timeout=10)
|
||||||
@@ -649,11 +663,14 @@ def _quota_usage():
|
|||||||
@app.route('/api/dashboard', methods=['GET'])
|
@app.route('/api/dashboard', methods=['GET'])
|
||||||
@require_auth
|
@require_auth
|
||||||
def get_dashboard():
|
def get_dashboard():
|
||||||
|
quota_ok, quota_reason = _quota_supported()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': _daemon_status(),
|
'status': _daemon_status(),
|
||||||
'disk': _disk_usage(),
|
'disk': _disk_usage(),
|
||||||
'shares': _shares_with_usage(),
|
'shares': _shares_with_usage(),
|
||||||
'quotas': _quota_usage(),
|
'quotas': _quota_usage() if quota_ok else [],
|
||||||
|
'quota_supported': quota_ok,
|
||||||
|
'quota_reason': quota_reason,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,14 @@ async function loadDashboard() {
|
|||||||
</tr>`).join('');
|
</tr>`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist quota support flag for shares.js to read
|
||||||
|
window._quotaSupported = d.quota_supported !== false;
|
||||||
|
window._quotaReason = d.quota_reason || '';
|
||||||
|
|
||||||
const qb = document.getElementById('dashQuotasBody');
|
const qb = document.getElementById('dashQuotasBody');
|
||||||
if (!d.quotas?.length) {
|
if (!d.quota_supported) {
|
||||||
|
qb.innerHTML = `<tr><td colspan="4" style="color:var(--muted); font-size:.87rem">${esc(d.quota_reason)}</td></tr>`;
|
||||||
|
} else if (!d.quotas?.length) {
|
||||||
qb.innerHTML = `<tr><td colspan="4" style="color:var(--muted)">No quotas set.</td></tr>`;
|
qb.innerHTML = `<tr><td colspan="4" style="color:var(--muted)">No quotas set.</td></tr>`;
|
||||||
} else {
|
} else {
|
||||||
qb.innerHTML = d.quotas.map(q => {
|
qb.innerHTML = d.quotas.map(q => {
|
||||||
|
|||||||
+4
-4
@@ -23,8 +23,8 @@ async function loadShares() {
|
|||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||||
onclick="openEditModal('smb',${shareJson})">Edit</button>
|
onclick="openEditModal('smb',${shareJson})">Edit</button>
|
||||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
${window._quotaSupported !== false ? `<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||||
onclick="openQuotaModal('smb',${nameJson},${pathJson},${usersJson})">Quota</button>
|
onclick="openQuotaModal('smb',${nameJson},${pathJson},${usersJson})">Quota</button>` : ''}
|
||||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||||
onclick="toggleShare('smb',${nameJson},${!enabled})">${enabled ? 'Disable' : 'Enable'}</button>
|
onclick="toggleShare('smb',${nameJson},${!enabled})">${enabled ? 'Disable' : 'Enable'}</button>
|
||||||
<button class="btn btn-sm btn-danger"
|
<button class="btn btn-sm btn-danger"
|
||||||
@@ -50,8 +50,8 @@ async function loadShares() {
|
|||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||||
onclick="openEditModal('nfs',${shareJson})">Edit</button>
|
onclick="openEditModal('nfs',${shareJson})">Edit</button>
|
||||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
${window._quotaSupported !== false ? `<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||||
onclick="openQuotaModal('nfs',null,${pathJson},null)">Quota</button>
|
onclick="openQuotaModal('nfs',null,${pathJson},null)">Quota</button>` : ''}
|
||||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||||
onclick="toggleShare('nfs',${pathJson},${!enabled})">${enabled ? 'Disable' : 'Enable'}</button>
|
onclick="toggleShare('nfs',${pathJson},${!enabled})">${enabled ? 'Disable' : 'Enable'}</button>
|
||||||
<button class="btn btn-sm btn-danger"
|
<button class="btn btn-sm btn-danger"
|
||||||
|
|||||||
Reference in new issue
Block a user