mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-28 14:10:40 +00:00
update storage settings
This commit is contained in:
@@ -326,3 +326,133 @@ def save_health_settings():
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ── Remote Storage Exclusions Endpoints ──
|
||||
|
||||
@health_bp.route('/api/health/remote-storages', methods=['GET'])
|
||||
def get_remote_storages():
|
||||
"""
|
||||
Get list of all remote storages with their exclusion status.
|
||||
Remote storages are those that can be offline (PBS, NFS, CIFS, etc.)
|
||||
"""
|
||||
try:
|
||||
from proxmox_storage_monitor import proxmox_storage_monitor
|
||||
|
||||
# Get current storage status
|
||||
storage_status = proxmox_storage_monitor.get_storage_status()
|
||||
all_storages = storage_status.get('available', []) + storage_status.get('unavailable', [])
|
||||
|
||||
# Filter to only remote types
|
||||
remote_types = health_persistence.REMOTE_STORAGE_TYPES
|
||||
remote_storages = [s for s in all_storages if s.get('type', '').lower() in remote_types]
|
||||
|
||||
# Get current exclusions
|
||||
exclusions = {e['storage_name']: e for e in health_persistence.get_excluded_storages()}
|
||||
|
||||
# Combine info
|
||||
result = []
|
||||
for storage in remote_storages:
|
||||
name = storage.get('name', '')
|
||||
exclusion = exclusions.get(name, {})
|
||||
result.append({
|
||||
'name': name,
|
||||
'type': storage.get('type', 'unknown'),
|
||||
'status': storage.get('status', 'unknown'),
|
||||
'total': storage.get('total', 0),
|
||||
'used': storage.get('used', 0),
|
||||
'available': storage.get('available', 0),
|
||||
'percent': storage.get('percent', 0),
|
||||
'exclude_health': exclusion.get('exclude_health', 0) == 1,
|
||||
'exclude_notifications': exclusion.get('exclude_notifications', 0) == 1,
|
||||
'excluded_at': exclusion.get('excluded_at'),
|
||||
'reason': exclusion.get('reason')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'storages': result,
|
||||
'remote_types': list(remote_types)
|
||||
})
|
||||
except ImportError:
|
||||
return jsonify({'error': 'Storage monitor not available', 'storages': []}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@health_bp.route('/api/health/storage-exclusions', methods=['GET'])
|
||||
def get_storage_exclusions():
|
||||
"""Get all storage exclusions."""
|
||||
try:
|
||||
exclusions = health_persistence.get_excluded_storages()
|
||||
return jsonify({'exclusions': exclusions})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@health_bp.route('/api/health/storage-exclusions', methods=['POST'])
|
||||
def save_storage_exclusion():
|
||||
"""
|
||||
Add or update a storage exclusion.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"storage_name": "pbs-backup",
|
||||
"storage_type": "pbs",
|
||||
"exclude_health": true,
|
||||
"exclude_notifications": true,
|
||||
"reason": "PBS server is offline daily"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'storage_name' not in data:
|
||||
return jsonify({'error': 'storage_name is required'}), 400
|
||||
|
||||
storage_name = data['storage_name']
|
||||
storage_type = data.get('storage_type', 'unknown')
|
||||
exclude_health = data.get('exclude_health', True)
|
||||
exclude_notifications = data.get('exclude_notifications', True)
|
||||
reason = data.get('reason')
|
||||
|
||||
# Check if already excluded
|
||||
existing = health_persistence.get_excluded_storages()
|
||||
exists = any(e['storage_name'] == storage_name for e in existing)
|
||||
|
||||
if exists:
|
||||
# Update existing
|
||||
success = health_persistence.update_storage_exclusion(
|
||||
storage_name, exclude_health, exclude_notifications
|
||||
)
|
||||
else:
|
||||
# Add new
|
||||
success = health_persistence.exclude_storage(
|
||||
storage_name, storage_type, exclude_health, exclude_notifications, reason
|
||||
)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Storage {storage_name} exclusion saved',
|
||||
'storage_name': storage_name
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': 'Failed to save exclusion'}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@health_bp.route('/api/health/storage-exclusions/<storage_name>', methods=['DELETE'])
|
||||
def delete_storage_exclusion(storage_name):
|
||||
"""Remove a storage from the exclusion list."""
|
||||
try:
|
||||
success = health_persistence.remove_storage_exclusion(storage_name)
|
||||
if success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Storage {storage_name} removed from exclusions'
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': 'Storage not found in exclusions'}), 404
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@@ -130,9 +130,10 @@ def get_ollama_models():
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
result = json.loads(resp.read().decode('utf-8'))
|
||||
models = [m.get('name', '').split(':')[0] for m in result.get('models', [])]
|
||||
# Remove duplicates and sort
|
||||
models = sorted(list(set(models)))
|
||||
# Keep full model names (including tags like :latest, :3b-instruct-q4_0)
|
||||
models = [m.get('name', '') for m in result.get('models', []) if m.get('name')]
|
||||
# Sort alphabetically
|
||||
models = sorted(models)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'models': models,
|
||||
|
||||
@@ -2639,6 +2639,24 @@ def get_proxmox_storage():
|
||||
for unavailable_storage in unavailable_storages:
|
||||
if unavailable_storage['name'] not in existing_storage_names:
|
||||
storage_list.append(unavailable_storage)
|
||||
|
||||
# Get storage exclusions to mark excluded storages
|
||||
try:
|
||||
excluded_health = health_persistence.get_excluded_storage_names('health')
|
||||
remote_types = health_persistence.REMOTE_STORAGE_TYPES
|
||||
|
||||
for storage in storage_list:
|
||||
storage_name = storage.get('name', '')
|
||||
storage_type = storage.get('type', '').lower()
|
||||
|
||||
# Mark if this is a remote storage type
|
||||
storage['is_remote'] = storage_type in remote_types
|
||||
|
||||
# Mark if excluded from health monitoring
|
||||
storage['excluded'] = storage_name in excluded_health
|
||||
except Exception:
|
||||
# If exclusion check fails, continue without it
|
||||
pass
|
||||
|
||||
return {'storage': storage_list}
|
||||
|
||||
|
||||
@@ -4431,6 +4431,8 @@ class HealthMonitor:
|
||||
Detects unavailable storages configured in PVE.
|
||||
Returns CRITICAL if any configured storage is unavailable.
|
||||
Returns None if the module is not available.
|
||||
|
||||
Respects storage exclusions: excluded storages are reported as INFO, not CRITICAL.
|
||||
"""
|
||||
if not PROXMOX_STORAGE_AVAILABLE:
|
||||
return None
|
||||
@@ -4443,12 +4445,22 @@ class HealthMonitor:
|
||||
storage_status = proxmox_storage_monitor.get_storage_status()
|
||||
unavailable_storages = storage_status.get('unavailable', [])
|
||||
|
||||
if not unavailable_storages:
|
||||
# All storages are available. We should also clear any previously recorded storage errors.
|
||||
# Get excluded storage names for health monitoring
|
||||
excluded_names = health_persistence.get_excluded_storage_names('health')
|
||||
|
||||
# Separate excluded storages from real issues
|
||||
excluded_unavailable = [s for s in unavailable_storages if s.get('name', '') in excluded_names]
|
||||
real_unavailable = [s for s in unavailable_storages if s.get('name', '') not in excluded_names]
|
||||
|
||||
if not real_unavailable:
|
||||
# All non-excluded storages are available. Clear any previously recorded storage errors.
|
||||
active_errors = health_persistence.get_active_errors()
|
||||
for error in active_errors:
|
||||
if error.get('category') == 'storage' and error.get('error_key', '').startswith('storage_unavailable_'):
|
||||
health_persistence.clear_error(error['error_key'])
|
||||
# Only clear if not an excluded storage
|
||||
storage_name = error.get('error_key', '').replace('storage_unavailable_', '')
|
||||
if storage_name not in excluded_names:
|
||||
health_persistence.clear_error(error['error_key'])
|
||||
|
||||
# Build checks from all configured storages for descriptive display
|
||||
available_storages = storage_status.get('available', [])
|
||||
@@ -4460,12 +4472,24 @@ class HealthMonitor:
|
||||
'status': 'OK',
|
||||
'detail': f'{st_type} storage available'
|
||||
}
|
||||
|
||||
# Add excluded unavailable storages as INFO (not CRITICAL)
|
||||
for st in excluded_unavailable:
|
||||
st_name = st.get('name', 'unknown')
|
||||
st_type = st.get('type', 'unknown')
|
||||
checks[st_name] = {
|
||||
'status': 'INFO',
|
||||
'detail': f'{st_type} storage excluded from monitoring',
|
||||
'excluded': True
|
||||
}
|
||||
|
||||
if not checks:
|
||||
checks['proxmox_storages'] = {'status': 'OK', 'detail': 'All storages available'}
|
||||
return {'status': 'OK', 'checks': checks}
|
||||
|
||||
storage_details = {}
|
||||
for storage in unavailable_storages:
|
||||
# Only process non-excluded unavailable storages as errors
|
||||
for storage in real_unavailable:
|
||||
storage_name = storage['name']
|
||||
error_key = f'storage_unavailable_{storage_name}'
|
||||
status_detail = storage.get('status_detail', 'unavailable')
|
||||
@@ -4508,6 +4532,17 @@ class HealthMonitor:
|
||||
'detail': st_info.get('reason', 'Unavailable'),
|
||||
'dismissable': False
|
||||
}
|
||||
|
||||
# Add excluded unavailable storages as INFO (not as errors)
|
||||
for st in excluded_unavailable:
|
||||
st_name = st.get('name', 'unknown')
|
||||
st_type = st.get('type', 'unknown')
|
||||
checks[st_name] = {
|
||||
'status': 'INFO',
|
||||
'detail': f'{st_type} storage excluded from monitoring (offline)',
|
||||
'excluded': True
|
||||
}
|
||||
|
||||
# Also add available storages
|
||||
available_list = storage_status.get('available', [])
|
||||
unavail_names = {s['name'] for s in unavailable_storages}
|
||||
@@ -4518,12 +4553,21 @@ class HealthMonitor:
|
||||
'detail': f'{st.get("type", "unknown")} storage available'
|
||||
}
|
||||
|
||||
return {
|
||||
'status': 'CRITICAL',
|
||||
'reason': f'{len(unavailable_storages)} Proxmox storage(s) unavailable',
|
||||
'details': storage_details,
|
||||
'checks': checks
|
||||
}
|
||||
# Determine overall status based on non-excluded issues only
|
||||
if real_unavailable:
|
||||
return {
|
||||
'status': 'CRITICAL',
|
||||
'reason': f'{len(real_unavailable)} Proxmox storage(s) unavailable',
|
||||
'details': storage_details,
|
||||
'checks': checks
|
||||
}
|
||||
else:
|
||||
# Only excluded storages are unavailable - this is OK
|
||||
return {
|
||||
'status': 'OK',
|
||||
'reason': 'All monitored storages available',
|
||||
'checks': checks
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[HealthMonitor] Error checking Proxmox storage: {e}")
|
||||
|
||||
@@ -235,6 +235,22 @@ class HealthPersistence:
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_obs_disk ON disk_observations(disk_registry_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_obs_dismissed ON disk_observations(dismissed)')
|
||||
|
||||
# ── Remote Storage Exclusions System ──
|
||||
# Allows users to permanently exclude remote storages (PBS, NFS, CIFS, etc.)
|
||||
# from health monitoring and notifications
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS excluded_storages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storage_name TEXT UNIQUE NOT NULL,
|
||||
storage_type TEXT NOT NULL,
|
||||
excluded_at TEXT NOT NULL,
|
||||
exclude_health INTEGER DEFAULT 1,
|
||||
exclude_notifications INTEGER DEFAULT 1,
|
||||
reason TEXT
|
||||
)
|
||||
''')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_excluded_storage ON excluded_storages(storage_name)')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -1845,5 +1861,172 @@ class HealthPersistence:
|
||||
return 0
|
||||
|
||||
|
||||
# ── Remote Storage Exclusions Methods ──
|
||||
|
||||
# Types considered "remote" and eligible for exclusion
|
||||
REMOTE_STORAGE_TYPES = {'pbs', 'nfs', 'cifs', 'glusterfs', 'iscsi', 'iscsidirect', 'cephfs', 'rbd'}
|
||||
|
||||
def is_remote_storage_type(self, storage_type: str) -> bool:
|
||||
"""Check if a storage type is considered remote/external."""
|
||||
return storage_type.lower() in self.REMOTE_STORAGE_TYPES
|
||||
|
||||
def get_excluded_storages(self) -> List[Dict[str, Any]]:
|
||||
"""Get list of all excluded remote storages."""
|
||||
try:
|
||||
with self._db_connection(row_factory=True) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT storage_name, storage_type, excluded_at,
|
||||
exclude_health, exclude_notifications, reason
|
||||
FROM excluded_storages
|
||||
''')
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
print(f"[HealthPersistence] Error getting excluded storages: {e}")
|
||||
return []
|
||||
|
||||
def is_storage_excluded(self, storage_name: str, check_type: str = 'health') -> bool:
|
||||
"""
|
||||
Check if a storage is excluded from monitoring.
|
||||
|
||||
Args:
|
||||
storage_name: Name of the storage
|
||||
check_type: 'health' or 'notifications'
|
||||
|
||||
Returns:
|
||||
True if storage is excluded for the given check type
|
||||
"""
|
||||
try:
|
||||
with self._db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
column = 'exclude_health' if check_type == 'health' else 'exclude_notifications'
|
||||
cursor.execute(f'''
|
||||
SELECT {column} FROM excluded_storages
|
||||
WHERE storage_name = ?
|
||||
''', (storage_name,))
|
||||
row = cursor.fetchone()
|
||||
return row is not None and row[0] == 1
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def exclude_storage(self, storage_name: str, storage_type: str,
|
||||
exclude_health: bool = True, exclude_notifications: bool = True,
|
||||
reason: str = None) -> bool:
|
||||
"""
|
||||
Add a storage to the exclusion list.
|
||||
|
||||
Args:
|
||||
storage_name: Name of the storage to exclude
|
||||
storage_type: Type of storage (pbs, nfs, etc.)
|
||||
exclude_health: Whether to exclude from health monitoring
|
||||
exclude_notifications: Whether to exclude from notifications
|
||||
reason: Optional reason for exclusion
|
||||
|
||||
Returns:
|
||||
True if successfully excluded
|
||||
"""
|
||||
try:
|
||||
now = datetime.now().isoformat()
|
||||
with self._db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO excluded_storages
|
||||
(storage_name, storage_type, excluded_at, exclude_health, exclude_notifications, reason)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(storage_name) DO UPDATE SET
|
||||
exclude_health = excluded.exclude_health,
|
||||
exclude_notifications = excluded.exclude_notifications,
|
||||
reason = excluded.reason
|
||||
''', (storage_name, storage_type, now,
|
||||
1 if exclude_health else 0,
|
||||
1 if exclude_notifications else 0,
|
||||
reason))
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[HealthPersistence] Error excluding storage: {e}")
|
||||
return False
|
||||
|
||||
def update_storage_exclusion(self, storage_name: str,
|
||||
exclude_health: Optional[bool] = None,
|
||||
exclude_notifications: Optional[bool] = None) -> bool:
|
||||
"""
|
||||
Update exclusion settings for a storage.
|
||||
|
||||
Args:
|
||||
storage_name: Name of the storage
|
||||
exclude_health: New value for health exclusion (None = don't change)
|
||||
exclude_notifications: New value for notifications exclusion (None = don't change)
|
||||
|
||||
Returns:
|
||||
True if successfully updated
|
||||
"""
|
||||
try:
|
||||
with self._db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
updates = []
|
||||
values = []
|
||||
|
||||
if exclude_health is not None:
|
||||
updates.append('exclude_health = ?')
|
||||
values.append(1 if exclude_health else 0)
|
||||
|
||||
if exclude_notifications is not None:
|
||||
updates.append('exclude_notifications = ?')
|
||||
values.append(1 if exclude_notifications else 0)
|
||||
|
||||
if not updates:
|
||||
return True
|
||||
|
||||
values.append(storage_name)
|
||||
cursor.execute(f'''
|
||||
UPDATE excluded_storages
|
||||
SET {', '.join(updates)}
|
||||
WHERE storage_name = ?
|
||||
''', values)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except Exception as e:
|
||||
print(f"[HealthPersistence] Error updating storage exclusion: {e}")
|
||||
return False
|
||||
|
||||
def remove_storage_exclusion(self, storage_name: str) -> bool:
|
||||
"""Remove a storage from the exclusion list."""
|
||||
try:
|
||||
with self._db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM excluded_storages WHERE storage_name = ?
|
||||
''', (storage_name,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except Exception as e:
|
||||
print(f"[HealthPersistence] Error removing storage exclusion: {e}")
|
||||
return False
|
||||
|
||||
def get_excluded_storage_names(self, check_type: str = 'health') -> set:
|
||||
"""
|
||||
Get set of storage names excluded for a specific check type.
|
||||
|
||||
Args:
|
||||
check_type: 'health' or 'notifications'
|
||||
|
||||
Returns:
|
||||
Set of excluded storage names
|
||||
"""
|
||||
try:
|
||||
with self._db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
column = 'exclude_health' if check_type == 'health' else 'exclude_notifications'
|
||||
cursor.execute(f'''
|
||||
SELECT storage_name FROM excluded_storages
|
||||
WHERE {column} = 1
|
||||
''')
|
||||
return {row[0] for row in cursor.fetchall()}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
# Global instance
|
||||
health_persistence = HealthPersistence()
|
||||
|
||||
@@ -648,6 +648,19 @@ class NotificationManager:
|
||||
if self._is_backup_running():
|
||||
return
|
||||
|
||||
# Check storage exclusions for storage-related events.
|
||||
# If the storage is excluded from notifications, suppress the event entirely.
|
||||
_STORAGE_EVENTS = {'storage_unavailable', 'storage_low_space', 'storage_warning', 'storage_error'}
|
||||
if event.event_type in _STORAGE_EVENTS:
|
||||
storage_name = event.data.get('storage_name') or event.data.get('name')
|
||||
if storage_name:
|
||||
try:
|
||||
from health_persistence import health_persistence
|
||||
if health_persistence.is_storage_excluded(storage_name, 'notifications'):
|
||||
return # Storage is excluded from notifications, skip silently
|
||||
except Exception:
|
||||
pass # Continue if check fails
|
||||
|
||||
# Check cooldown
|
||||
if not self._check_cooldown(event):
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user