1711 lines
70 KiB
Python
1711 lines
70 KiB
Python
from flask import Flask, render_template, jsonify, request, redirect, url_for, session, flash, Response
|
|
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
|
from functools import wraps
|
|
import requests
|
|
import urllib3
|
|
import traceback
|
|
import json
|
|
import queue
|
|
import time
|
|
import subprocess
|
|
import platform
|
|
from datetime import datetime, timedelta
|
|
from config import Config
|
|
from models import User, UserVM, ActionLog, Subdomain, IPAssignment, get_db_connection
|
|
|
|
# Notification queues per user
|
|
notification_queues = {}
|
|
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(Config)
|
|
|
|
# Flask-Login setup
|
|
login_manager = LoginManager()
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = 'login'
|
|
login_manager.login_message = 'Effettua il login per accedere a questa pagina.'
|
|
|
|
# Proxmox API setup
|
|
BASE_URL = f"https://{Config.PROXMOX_IP}:{Config.PROXMOX_PORT}/api2/json"
|
|
HEADERS = {
|
|
"Authorization": f"PVEAPIToken={Config.API_TOKEN_ID}={Config.API_TOKEN_SECRET}"
|
|
}
|
|
|
|
# User class per Flask-Login
|
|
class FlaskUser(UserMixin):
|
|
def __init__(self, user_data):
|
|
self.id = user_data['id']
|
|
self.username = user_data['username']
|
|
self.email = user_data['email']
|
|
self.is_admin = user_data['is_admin']
|
|
|
|
@login_manager.user_loader
|
|
def load_user(user_id):
|
|
user_data = User.get_by_id(int(user_id))
|
|
if user_data:
|
|
return FlaskUser(user_data)
|
|
return None
|
|
|
|
# Decorator per verificare se l'utente è admin
|
|
def admin_required(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if not current_user.is_authenticated or not current_user.is_admin:
|
|
flash('Accesso negato. Privilegi di amministratore richiesti.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
# Helper per logging azioni
|
|
def log_user_action(vm_id, action_type, status, error_message=None):
|
|
try:
|
|
ActionLog.log_action(
|
|
user_id=current_user.id,
|
|
vm_id=vm_id,
|
|
action_type=action_type,
|
|
status=status,
|
|
error_message=error_message,
|
|
ip_address=request.remote_addr
|
|
)
|
|
except Exception as e:
|
|
app.logger.error(f"Errore logging azione: {e}")
|
|
|
|
# Helper per chiamate Proxmox API
|
|
def proxmox_api_call(method, endpoint, data=None, params=None, timeout=30):
|
|
"""Helper per chiamate all'API Proxmox"""
|
|
url = f"{BASE_URL}{endpoint}"
|
|
# Timeout più lungo per operazioni su storage di rete (CIFS/NFS)
|
|
if '/storage/' in endpoint and '/content' in endpoint:
|
|
timeout = 120
|
|
try:
|
|
if method.upper() == 'GET':
|
|
response = requests.get(url, headers=HEADERS, params=params, verify=False, timeout=timeout)
|
|
elif method.upper() == 'POST':
|
|
response = requests.post(url, headers=HEADERS, data=data, verify=False, timeout=timeout)
|
|
elif method.upper() == 'PUT':
|
|
response = requests.put(url, headers=HEADERS, data=data, verify=False, timeout=timeout)
|
|
elif method.upper() == 'DELETE':
|
|
response = requests.delete(url, headers=HEADERS, verify=False, timeout=timeout)
|
|
else:
|
|
return None, "Metodo HTTP non supportato"
|
|
|
|
response.raise_for_status()
|
|
return response.json(), None
|
|
except requests.exceptions.RequestException as e:
|
|
return None, str(e)
|
|
|
|
# ==================== ROUTES AUTENTICAZIONE ====================
|
|
|
|
@app.route('/')
|
|
def index():
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for('dashboard'))
|
|
return redirect(url_for('login'))
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
if current_user.is_authenticated:
|
|
return redirect(url_for('dashboard'))
|
|
|
|
if request.method == 'POST':
|
|
username = request.form.get('username')
|
|
password = request.form.get('password')
|
|
remember = request.form.get('remember', False)
|
|
|
|
if User.verify_password(username, password):
|
|
user_data = User.get_by_username(username)
|
|
user = FlaskUser(user_data)
|
|
login_user(user, remember=remember)
|
|
User.update_last_login(user.id)
|
|
|
|
# Log del login
|
|
ActionLog.log_action(user.id, 0, 'login', 'success', ip_address=request.remote_addr)
|
|
|
|
flash(f'Benvenuto, {user.username}!', 'success')
|
|
|
|
next_page = request.args.get('next')
|
|
return redirect(next_page or url_for('dashboard'))
|
|
else:
|
|
flash('Username o password non validi.', 'error')
|
|
|
|
return render_template('login.html')
|
|
|
|
@app.route('/logout')
|
|
@login_required
|
|
def logout():
|
|
logout_user()
|
|
flash('Logout effettuato con successo.', 'success')
|
|
return redirect(url_for('login'))
|
|
|
|
@app.route('/profile')
|
|
@login_required
|
|
def profile():
|
|
"""Pagina profilo utente"""
|
|
return render_template('profile.html', user=current_user)
|
|
|
|
@app.route('/domains')
|
|
@login_required
|
|
def domains():
|
|
"""Pagina gestione domini"""
|
|
return render_template('domains.html', user=current_user, domain=Config.CLOUDFLARE_DOMAIN)
|
|
|
|
@app.route('/api/profile/update-email', methods=['POST'])
|
|
@login_required
|
|
def update_email():
|
|
"""Aggiorna email utente"""
|
|
try:
|
|
data = request.get_json()
|
|
new_email = data.get('email')
|
|
password = data.get('password')
|
|
|
|
if not new_email or not password:
|
|
return jsonify({'status': 'error', 'message': 'Email e password richiesti'}), 400
|
|
|
|
# Verifica password corrente
|
|
if not User.verify_password(current_user.username, password):
|
|
return jsonify({'status': 'error', 'message': 'Password non corretta'}), 403
|
|
|
|
# Verifica che l'email non sia già usata
|
|
existing_user = User.get_by_username(new_email) # Check if email exists as username
|
|
conn = get_db_connection()
|
|
try:
|
|
with conn.cursor() as cursor:
|
|
cursor.execute("SELECT id FROM users WHERE email = %s AND id != %s", (new_email, current_user.id))
|
|
if cursor.fetchone():
|
|
return jsonify({'status': 'error', 'message': 'Email già in uso'}), 400
|
|
finally:
|
|
conn.close()
|
|
|
|
# Aggiorna email
|
|
if User.update_email(current_user.id, new_email):
|
|
return jsonify({'status': 'success', 'message': 'Email aggiornata con successo'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'Errore durante aggiornamento'}), 500
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore update_email: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/profile/update-password', methods=['POST'])
|
|
@login_required
|
|
def update_password():
|
|
"""Aggiorna password utente"""
|
|
try:
|
|
data = request.get_json()
|
|
current_password = data.get('current_password')
|
|
new_password = data.get('new_password')
|
|
confirm_password = data.get('confirm_password')
|
|
|
|
if not current_password or not new_password or not confirm_password:
|
|
return jsonify({'status': 'error', 'message': 'Tutti i campi sono richiesti'}), 400
|
|
|
|
# Verifica password corrente
|
|
if not User.verify_password(current_user.username, current_password):
|
|
return jsonify({'status': 'error', 'message': 'Password corrente non corretta'}), 403
|
|
|
|
# Verifica che le nuove password corrispondano
|
|
if new_password != confirm_password:
|
|
return jsonify({'status': 'error', 'message': 'Le nuove password non corrispondono'}), 400
|
|
|
|
# Verifica lunghezza minima
|
|
if len(new_password) < 6:
|
|
return jsonify({'status': 'error', 'message': 'La password deve essere di almeno 6 caratteri'}), 400
|
|
|
|
# Aggiorna password
|
|
if User.update_password(current_user.id, new_password):
|
|
return jsonify({'status': 'success', 'message': 'Password aggiornata con successo'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'Errore durante aggiornamento'}), 500
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore update_password: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/dashboard')
|
|
@login_required
|
|
def dashboard():
|
|
if current_user.is_admin:
|
|
return render_template('admin_dashboard.html', user=current_user)
|
|
else:
|
|
return render_template('user_dashboard.html', user=current_user)
|
|
|
|
# ==================== API ROUTES - VM MANAGEMENT ====================
|
|
|
|
@app.route('/api/my-vms')
|
|
@login_required
|
|
def get_my_vms():
|
|
"""Recupera le VM dell'utente loggato con stato da Proxmox"""
|
|
try:
|
|
user_vms = UserVM.get_user_vms(current_user.id)
|
|
app.logger.info(f"User {current_user.id} has {len(user_vms)} VMs assigned: {user_vms}")
|
|
|
|
# Recupera info da Proxmox per ogni VM
|
|
vms_with_status = []
|
|
for vm in user_vms:
|
|
vm_id = vm['vm_id']
|
|
|
|
# Cerca la VM su tutti i nodi
|
|
app.logger.info(f"Fetching nodes list from Proxmox...")
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
|
|
if error:
|
|
app.logger.error(f"Error fetching nodes: {error}")
|
|
continue
|
|
|
|
if not nodes_data or 'data' not in nodes_data:
|
|
app.logger.error(f"Invalid nodes response: {nodes_data}")
|
|
continue
|
|
|
|
app.logger.info(f"Found {len(nodes_data.get('data', []))} nodes: {[n.get('node') for n in nodes_data.get('data', [])]}")
|
|
|
|
vm_info = None
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
app.logger.info(f"Checking node: {node_name} for VM {vm_id}")
|
|
|
|
# Prova QEMU
|
|
qemu_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/qemu/{vm_id}/status/current')
|
|
if qemu_data and 'data' in qemu_data:
|
|
vm_info = qemu_data['data']
|
|
vm_info['type'] = 'qemu'
|
|
vm_info['node'] = node_name
|
|
app.logger.info(f"Found QEMU VM {vm_id} on node {node_name}")
|
|
break
|
|
|
|
# Prova LXC
|
|
lxc_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/lxc/{vm_id}/status/current')
|
|
if lxc_data and 'data' in lxc_data:
|
|
vm_info = lxc_data['data']
|
|
vm_info['type'] = 'lxc'
|
|
vm_info['node'] = node_name
|
|
app.logger.info(f"Found LXC VM {vm_id} on node {node_name}")
|
|
break
|
|
|
|
if vm_info:
|
|
vms_with_status.append({
|
|
'vm_id': vm_id,
|
|
'vm_name': vm['vm_name'] or vm_info.get('name', f'VM-{vm_id}'),
|
|
'notes': vm['notes'],
|
|
'ip_address': vm.get('ip_address'),
|
|
'status': vm_info.get('status', 'unknown'),
|
|
'uptime': vm_info.get('uptime', 0),
|
|
'cpu': vm_info.get('cpu', 0),
|
|
'mem': vm_info.get('mem', 0),
|
|
'maxmem': vm_info.get('maxmem', 0),
|
|
'disk': vm_info.get('disk', 0),
|
|
'maxdisk': vm_info.get('maxdisk', 0),
|
|
'type': vm_info['type'],
|
|
'node': vm_info['node']
|
|
})
|
|
else:
|
|
app.logger.warning(f"VM {vm_id} not found in Proxmox!")
|
|
vms_with_status.append({
|
|
'vm_id': vm_id,
|
|
'vm_name': vm['vm_name'] or f'VM-{vm_id}',
|
|
'notes': vm['notes'],
|
|
'ip_address': vm.get('ip_address'),
|
|
'status': 'unknown',
|
|
'type': 'unknown',
|
|
'node': 'unknown',
|
|
'cpu': 0,
|
|
'mem': 0,
|
|
'maxmem': 0,
|
|
'disk': 0,
|
|
'maxdisk': 0
|
|
})
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'data': vms_with_status
|
|
})
|
|
except Exception as e:
|
|
app.logger.error(f"Errore get_my_vms: {traceback.format_exc()}")
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/status')
|
|
@login_required
|
|
def get_vm_status(vm_id):
|
|
"""Recupera lo stato di una VM specifica con metriche real-time"""
|
|
try:
|
|
# Verifica che l'utente abbia accesso
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
# Cerca la VM su tutti i nodi
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
qemu_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/qemu/{vm_id}/status/current')
|
|
if qemu_data and 'data' in qemu_data:
|
|
vm_info = qemu_data['data']
|
|
|
|
# Recupera info sui dischi
|
|
config_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/qemu/{vm_id}/config')
|
|
if config_data and 'data' in config_data:
|
|
vm_info['config'] = config_data['data']
|
|
|
|
return jsonify({'status': 'success', 'data': vm_info, 'node': node_name, 'type': 'qemu'})
|
|
|
|
# Prova LXC
|
|
lxc_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/lxc/{vm_id}/status/current')
|
|
if lxc_data and 'data' in lxc_data:
|
|
vm_info = lxc_data['data']
|
|
|
|
# Recupera info sui dischi
|
|
config_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/lxc/{vm_id}/config')
|
|
if config_data and 'data' in config_data:
|
|
vm_info['config'] = config_data['data']
|
|
|
|
return jsonify({'status': 'success', 'data': vm_info, 'node': node_name, 'type': 'lxc'})
|
|
|
|
return jsonify({'status': 'error', 'message': 'VM non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore get_vm_status: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/rrddata')
|
|
@login_required
|
|
def get_vm_rrddata(vm_id):
|
|
"""Recupera dati RRD per grafici in tempo reale"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
timeframe = request.args.get('timeframe', 'hour')
|
|
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
rrd_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/qemu/{vm_id}/rrddata',
|
|
params={'timeframe': timeframe})
|
|
if rrd_data and 'data' in rrd_data:
|
|
return jsonify({'status': 'success', 'data': rrd_data['data']})
|
|
|
|
# Prova LXC
|
|
rrd_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/lxc/{vm_id}/rrddata',
|
|
params={'timeframe': timeframe})
|
|
if rrd_data and 'data' in rrd_data:
|
|
return jsonify({'status': 'success', 'data': rrd_data['data']})
|
|
|
|
return jsonify({'status': 'error', 'message': 'Dati non disponibili'}), 404
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore get_vm_rrddata: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
# ==================== SNAPSHOT MANAGEMENT ====================
|
|
|
|
@app.route('/api/vm/<int:vm_id>/snapshots')
|
|
@login_required
|
|
def get_vm_snapshots(vm_id):
|
|
"""Recupera le snapshot di una VM (max 3 per utenti normali)"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
snap_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/qemu/{vm_id}/snapshot')
|
|
if snap_data and 'data' in snap_data:
|
|
snapshots = [s for s in snap_data['data'] if s.get('name') != 'current']
|
|
|
|
# Limita a 3 per utenti normali
|
|
if not current_user.is_admin:
|
|
snapshots = snapshots[:3]
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'count': len(snapshots),
|
|
'data': snapshots,
|
|
'node': node_name,
|
|
'type': 'qemu'
|
|
})
|
|
|
|
# Prova LXC
|
|
snap_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/lxc/{vm_id}/snapshot')
|
|
if snap_data and 'data' in snap_data:
|
|
snapshots = [s for s in snap_data['data'] if s.get('name') != 'current']
|
|
|
|
if not current_user.is_admin:
|
|
snapshots = snapshots[:3]
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'count': len(snapshots),
|
|
'data': snapshots,
|
|
'node': node_name,
|
|
'type': 'lxc'
|
|
})
|
|
|
|
return jsonify({'status': 'success', 'count': 0, 'data': []}), 200
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore get_vm_snapshots: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/snapshot', methods=['POST'])
|
|
@login_required
|
|
def create_snapshot(vm_id):
|
|
"""Crea una snapshot di una VM"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
data = request.get_json()
|
|
snapname = data.get('snapname', f'snap-{datetime.now().strftime("%Y%m%d-%H%M%S")}')
|
|
description = data.get('description', '')
|
|
|
|
# Controlla se l'utente ha già 3 snapshot (se non admin)
|
|
if not current_user.is_admin:
|
|
nodes_data, _ = proxmox_api_call('GET', '/nodes')
|
|
if nodes_data:
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
snap_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/qemu/{vm_id}/snapshot')
|
|
if not snap_data:
|
|
snap_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/lxc/{vm_id}/snapshot')
|
|
|
|
if snap_data and 'data' in snap_data:
|
|
existing_snaps = [s for s in snap_data['data'] if s.get('name') != 'current']
|
|
if len(existing_snaps) >= 3:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': 'Hai raggiunto il limite di 3 snapshot. Elimina una snapshot esistente.'
|
|
}), 400
|
|
|
|
# Trova il nodo della VM e crea la snapshot
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
log_user_action(vm_id, 'snapshot', 'failed', error)
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/qemu/{vm_id}/snapshot',
|
|
data={'snapname': snapname, 'description': description})
|
|
if result:
|
|
log_user_action(vm_id, 'snapshot', 'success', error_message=f'Snapshot {snapname} creata')
|
|
return jsonify({'status': 'success', 'message': 'Snapshot creata con successo', 'snapname': snapname})
|
|
|
|
# Prova LXC
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/lxc/{vm_id}/snapshot',
|
|
data={'snapname': snapname, 'description': description})
|
|
if result:
|
|
log_user_action(vm_id, 'snapshot', 'success', error_message=f'Snapshot {snapname} creata')
|
|
return jsonify({'status': 'success', 'message': 'Snapshot creata con successo', 'snapname': snapname})
|
|
|
|
log_user_action(vm_id, 'snapshot', 'failed', 'VM non trovata')
|
|
return jsonify({'status': 'error', 'message': 'VM non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
log_user_action(vm_id, 'snapshot', 'failed', str(e))
|
|
app.logger.error(f"Errore create_snapshot: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/snapshot/<snapname>', methods=['DELETE'])
|
|
@login_required
|
|
def delete_snapshot(vm_id, snapname):
|
|
"""Elimina una snapshot"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
result, error = proxmox_api_call('DELETE', f'/nodes/{node_name}/qemu/{vm_id}/snapshot/{snapname}')
|
|
if result:
|
|
log_user_action(vm_id, 'snapshot', 'success', error_message=f'Snapshot {snapname} eliminata')
|
|
return jsonify({'status': 'success', 'message': 'Snapshot eliminata con successo'})
|
|
|
|
# Prova LXC
|
|
result, error = proxmox_api_call('DELETE', f'/nodes/{node_name}/lxc/{vm_id}/snapshot/{snapname}')
|
|
if result:
|
|
log_user_action(vm_id, 'snapshot', 'success', error_message=f'Snapshot {snapname} eliminata')
|
|
return jsonify({'status': 'success', 'message': 'Snapshot eliminata con successo'})
|
|
|
|
return jsonify({'status': 'error', 'message': 'Snapshot non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore delete_snapshot: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/shutdown', methods=['POST'])
|
|
@login_required
|
|
def shutdown_vm(vm_id):
|
|
"""Spegne una VM (graceful shutdown)"""
|
|
try:
|
|
# Verifica accesso
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
# Trova il nodo della VM
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
log_user_action(vm_id, 'shutdown', 'failed', error)
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/qemu/{vm_id}/status/shutdown')
|
|
if result:
|
|
log_user_action(vm_id, 'shutdown', 'success')
|
|
send_notification(current_user.id, 'success', f'VM {vm_id} in fase di spegnimento')
|
|
return jsonify({'status': 'success', 'message': 'VM spenta con successo'})
|
|
|
|
# Prova LXC
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/lxc/{vm_id}/status/shutdown')
|
|
if result:
|
|
log_user_action(vm_id, 'shutdown', 'success')
|
|
send_notification(current_user.id, 'success', f'Container {vm_id} in fase di spegnimento')
|
|
return jsonify({'status': 'success', 'message': 'Container spento con successo'})
|
|
|
|
log_user_action(vm_id, 'shutdown', 'failed', 'VM non trovata')
|
|
return jsonify({'status': 'error', 'message': 'VM non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
log_user_action(vm_id, 'shutdown', 'failed', str(e))
|
|
app.logger.error(f"Errore shutdown_vm: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/stop', methods=['POST'])
|
|
@login_required
|
|
def stop_vm(vm_id):
|
|
"""Ferma forzatamente una VM (hard stop)"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
log_user_action(vm_id, 'stop', 'failed', error)
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/qemu/{vm_id}/status/stop')
|
|
if result:
|
|
log_user_action(vm_id, 'stop', 'success')
|
|
return jsonify({'status': 'success', 'message': 'VM fermata con successo'})
|
|
|
|
# Prova LXC
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/lxc/{vm_id}/status/stop')
|
|
if result:
|
|
log_user_action(vm_id, 'stop', 'success')
|
|
return jsonify({'status': 'success', 'message': 'Container fermato con successo'})
|
|
|
|
log_user_action(vm_id, 'stop', 'failed', 'VM non trovata')
|
|
return jsonify({'status': 'error', 'message': 'VM non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
log_user_action(vm_id, 'stop', 'failed', str(e))
|
|
app.logger.error(f"Errore stop_vm: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/start', methods=['POST'])
|
|
@login_required
|
|
def start_vm(vm_id):
|
|
"""Avvia una VM"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
log_user_action(vm_id, 'start', 'failed', error)
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/qemu/{vm_id}/status/start')
|
|
if result:
|
|
log_user_action(vm_id, 'start', 'success')
|
|
send_notification(current_user.id, 'success', f'VM {vm_id} avviata con successo')
|
|
return jsonify({'status': 'success', 'message': 'VM avviata con successo'})
|
|
|
|
# Prova LXC
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/lxc/{vm_id}/status/start')
|
|
if result:
|
|
log_user_action(vm_id, 'start', 'success')
|
|
send_notification(current_user.id, 'success', f'Container {vm_id} avviato con successo')
|
|
return jsonify({'status': 'success', 'message': 'Container avviato con successo'})
|
|
|
|
log_user_action(vm_id, 'start', 'failed', 'VM non trovata')
|
|
return jsonify({'status': 'error', 'message': 'VM non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
log_user_action(vm_id, 'start', 'failed', str(e))
|
|
app.logger.error(f"Errore start_vm: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/reboot', methods=['POST'])
|
|
@login_required
|
|
def reboot_vm(vm_id):
|
|
"""Riavvia una VM"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
log_user_action(vm_id, 'restart', 'failed', error)
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/qemu/{vm_id}/status/reboot')
|
|
if result:
|
|
log_user_action(vm_id, 'restart', 'success')
|
|
send_notification(current_user.id, 'success', f'VM {vm_id} riavviata con successo')
|
|
return jsonify({'status': 'success', 'message': 'VM riavviata con successo'})
|
|
|
|
# Prova LXC
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/lxc/{vm_id}/status/reboot')
|
|
if result:
|
|
log_user_action(vm_id, 'restart', 'success')
|
|
send_notification(current_user.id, 'success', f'Container {vm_id} riavviato con successo')
|
|
return jsonify({'status': 'success', 'message': 'Container riavviato con successo'})
|
|
|
|
log_user_action(vm_id, 'restart', 'failed', 'VM non trovata')
|
|
return jsonify({'status': 'error', 'message': 'VM non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
log_user_action(vm_id, 'restart', 'failed', str(e))
|
|
app.logger.error(f"Errore reboot_vm: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
# ==================== API ROUTES - BACKUP MANAGEMENT ====================
|
|
|
|
@app.route('/api/vm/<int:vm_id>/backups')
|
|
@login_required
|
|
def get_vm_backups(vm_id):
|
|
"""Recupera i backup di una VM specifica (max 3 per utenti normali)"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
vm_backups = []
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
app.logger.info(f"[BACKUP DEBUG] Checking node: {node_name}")
|
|
|
|
storage_data, storage_error = proxmox_api_call('GET', f'/nodes/{node_name}/storage')
|
|
if not storage_data:
|
|
app.logger.warning(f"[BACKUP DEBUG] No storage data for node {node_name}, error: {storage_error}")
|
|
continue
|
|
|
|
for storage in storage_data.get('data', []):
|
|
storage_name = storage.get('storage')
|
|
storage_content = storage.get('content', '')
|
|
app.logger.info(f"[BACKUP DEBUG] Storage: {storage_name}, content: {storage_content}")
|
|
|
|
if not storage_name or 'backup' not in storage_content:
|
|
app.logger.info(f"[BACKUP DEBUG] Skipping storage {storage_name} - no backup content")
|
|
continue
|
|
|
|
content_data, content_error = proxmox_api_call('GET', f'/nodes/{node_name}/storage/{storage_name}/content', params={'content': 'backup'})
|
|
if not content_data:
|
|
app.logger.warning(f"[BACKUP DEBUG] No content for storage {storage_name}, error: {content_error}")
|
|
continue
|
|
|
|
backups = content_data.get('data', [])
|
|
app.logger.info(f"[BACKUP DEBUG] Storage {storage_name} has {len(backups)} backups")
|
|
|
|
for backup in backups:
|
|
volid = backup.get('volid', '')
|
|
app.logger.info(f"[BACKUP DEBUG] Checking backup volid: {volid} for VM {vm_id}")
|
|
if f"-{vm_id}-" in volid:
|
|
backup['node'] = node_name
|
|
backup['storage'] = storage_name
|
|
vm_backups.append(backup)
|
|
app.logger.info(f"[BACKUP DEBUG] MATCH! Added backup for VM {vm_id}")
|
|
|
|
# Ordina per data (più recente prima)
|
|
vm_backups.sort(key=lambda x: x.get('ctime', 0), reverse=True)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'vmid': vm_id,
|
|
'count': len(vm_backups),
|
|
'data': vm_backups
|
|
})
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore get_vm_backups: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/backup', methods=['POST'])
|
|
@login_required
|
|
def create_backup(vm_id):
|
|
"""Crea un backup di una VM"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
# Controlla se l'utente ha già 3 backup (se non admin)
|
|
if not current_user.is_admin:
|
|
nodes_data, _ = proxmox_api_call('GET', '/nodes')
|
|
if nodes_data:
|
|
backup_count = 0
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
storage_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/storage')
|
|
if storage_data:
|
|
for storage in storage_data.get('data', []):
|
|
storage_name = storage.get('storage')
|
|
if storage_name and 'backup' in storage.get('content', ''):
|
|
content_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/storage/{storage_name}/content', params={'content': 'backup'})
|
|
if content_data:
|
|
for backup in content_data.get('data', []):
|
|
if f"-{vm_id}-" in backup.get('volid', ''):
|
|
backup_count += 1
|
|
|
|
if backup_count >= Config.MAX_BACKUPS_PER_USER:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': f'Hai raggiunto il limite di {Config.MAX_BACKUPS_PER_USER} backup. Elimina un backup esistente prima di crearne uno nuovo.'
|
|
}), 400
|
|
|
|
# Trova il nodo della VM e crea il backup
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
log_user_action(vm_id, 'backup', 'failed', error)
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Verifica che la VM esista
|
|
qemu_exists, _ = proxmox_api_call('GET', f'/nodes/{node_name}/qemu/{vm_id}/status/current')
|
|
lxc_exists, _ = proxmox_api_call('GET', f'/nodes/{node_name}/lxc/{vm_id}/status/current')
|
|
|
|
if qemu_exists or lxc_exists:
|
|
# Usa modalità snapshot: Proxmox crea snapshot temporanea, fa backup, e la rimuove automaticamente
|
|
backup_data = {
|
|
'vmid': vm_id,
|
|
'mode': 'snapshot', # Crea snapshot temporanea automaticamente
|
|
'compress': 'zstd',
|
|
'storage': 'hetzner-backup01',
|
|
'remove': 1 # IMPORTANTE: Rimuove la snapshot temporanea dopo il backup
|
|
}
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/vzdump', data=backup_data)
|
|
if result:
|
|
log_user_action(vm_id, 'backup', 'success')
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Backup avviato! La VM rimarrà in esecuzione. La snapshot temporanea verrà rimossa automaticamente.',
|
|
'data': result
|
|
})
|
|
else:
|
|
log_user_action(vm_id, 'backup', 'failed', error)
|
|
return jsonify({'status': 'error', 'message': error or 'Errore durante creazione backup'}), 500
|
|
|
|
log_user_action(vm_id, 'backup', 'failed', 'VM non trovata')
|
|
return jsonify({'status': 'error', 'message': 'VM non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
log_user_action(vm_id, 'backup', 'failed', str(e))
|
|
app.logger.error(f"Errore create_backup: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/backup/<node>/<storage>/<path:volid>/delete', methods=['DELETE'])
|
|
@login_required
|
|
def delete_backup(node, storage, volid):
|
|
"""Elimina un backup"""
|
|
try:
|
|
# Estrai VM ID dal volid per verificare permessi
|
|
# Format: storage:backup/vzdump-qemu-104-2025_11_24-14_30_00.vma.zst
|
|
vm_id_match = volid.split('-')
|
|
if len(vm_id_match) >= 3:
|
|
try:
|
|
vm_id = int(vm_id_match[2])
|
|
except (ValueError, IndexError):
|
|
return jsonify({'status': 'error', 'message': 'VM ID non valido nel backup'}), 400
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'Formato backup non valido'}), 400
|
|
|
|
# Verifica accesso
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
# Elimina il backup
|
|
full_volid = f"{storage}:backup/{volid}" if not volid.startswith(storage) else volid
|
|
|
|
result, error = proxmox_api_call('DELETE', f'/nodes/{node}/storage/{storage}/content/{full_volid}')
|
|
|
|
if result:
|
|
log_user_action(vm_id, 'backup', 'success', error_message=f'Backup {volid} eliminato')
|
|
return jsonify({'status': 'success', 'message': 'Backup eliminato con successo'})
|
|
else:
|
|
log_user_action(vm_id, 'backup', 'failed', error)
|
|
return jsonify({'status': 'error', 'message': error or 'Errore durante eliminazione'}), 500
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore delete_backup: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/vm/<int:vm_id>/console')
|
|
@login_required
|
|
def get_vm_console(vm_id):
|
|
"""Ottieni URL console VNC/SPICE per la VM"""
|
|
try:
|
|
if not current_user.is_admin and not UserVM.user_has_vm(current_user.id, vm_id):
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU - usa vncproxy
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/qemu/{vm_id}/vncproxy',
|
|
data={'websocket': 1})
|
|
if result and 'data' in result:
|
|
console_data = result['data']
|
|
ticket = console_data.get('ticket', '')
|
|
port = console_data.get('port', '')
|
|
upid = console_data.get('upid', '')
|
|
|
|
# Costruisci URL per noVNC - usa URL pubblico via reverse proxy
|
|
console_url = (
|
|
f"{Config.PROXMOX_PUBLIC_URL}/"
|
|
f"?console=kvm&novnc=1&vmid={vm_id}&vmname=VM-{vm_id}"
|
|
f"&node={node_name}&resize=scale&cmd="
|
|
)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'console_url': console_url,
|
|
'ticket': ticket,
|
|
'type': 'vnc',
|
|
'data': console_data
|
|
})
|
|
|
|
# Prova LXC - usa termproxy
|
|
result, error = proxmox_api_call('POST', f'/nodes/{node_name}/lxc/{vm_id}/termproxy')
|
|
if result and 'data' in result:
|
|
console_data = result['data']
|
|
ticket = console_data.get('ticket', '')
|
|
port = console_data.get('port', '')
|
|
|
|
# Costruisci URL per xterm.js - usa URL pubblico via reverse proxy
|
|
console_url = (
|
|
f"{Config.PROXMOX_PUBLIC_URL}/"
|
|
f"?console=lxc&xtermjs=1&vmid={vm_id}&vmname=CT-{vm_id}"
|
|
f"&node={node_name}&resize=scale&cmd="
|
|
)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'console_url': console_url,
|
|
'ticket': ticket,
|
|
'type': 'shell',
|
|
'data': console_data
|
|
})
|
|
|
|
return jsonify({'status': 'error', 'message': 'VM non trovata'}), 404
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore get_vm_console: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
# ==================== SUBDOMAIN MANAGEMENT (CLOUDFLARE) ====================
|
|
|
|
@app.route('/api/subdomains')
|
|
@login_required
|
|
def get_user_subdomains():
|
|
"""Recupera i sottodomini dell'utente loggato"""
|
|
try:
|
|
subdomains = Subdomain.get_user_subdomains(current_user.id)
|
|
return jsonify({'status': 'success', 'data': subdomains})
|
|
except Exception as e:
|
|
app.logger.error(f"Errore get_user_subdomains: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/subdomain/create', methods=['POST'])
|
|
@login_required
|
|
def create_subdomain():
|
|
"""Crea un nuovo sottodominio su Cloudflare"""
|
|
try:
|
|
data = request.get_json()
|
|
subdomain = data.get('subdomain', '').strip().lower()
|
|
ip_address = data.get('ip_address', '').strip()
|
|
vm_id = data.get('vm_id')
|
|
proxied = data.get('proxied', True)
|
|
|
|
if not subdomain or not ip_address:
|
|
return jsonify({'status': 'error', 'message': 'Sottodominio e IP richiesti'}), 400
|
|
|
|
# Validazione sottodominio (solo caratteri alfanumerici e trattini)
|
|
import re
|
|
if not re.match(r'^[a-z0-9-]+$', subdomain):
|
|
return jsonify({'status': 'error', 'message': 'Sottodominio non valido. Usa solo lettere, numeri e trattini.'}), 400
|
|
|
|
# Verifica se esiste già
|
|
if Subdomain.subdomain_exists(subdomain):
|
|
return jsonify({'status': 'error', 'message': 'Questo sottodominio esiste già'}), 400
|
|
|
|
# Chiama API Cloudflare
|
|
if not Config.CLOUDFLARE_API_TOKEN or not Config.CLOUDFLARE_ZONE_ID:
|
|
return jsonify({'status': 'error', 'message': 'Cloudflare non configurato'}), 500
|
|
|
|
import cloudflare
|
|
cf = cloudflare.Cloudflare(api_token=Config.CLOUDFLARE_API_TOKEN)
|
|
|
|
full_domain = f'{subdomain}.{Config.CLOUDFLARE_DOMAIN}'
|
|
|
|
# Crea record DNS su Cloudflare
|
|
record = cf.dns.records.create(
|
|
zone_id=Config.CLOUDFLARE_ZONE_ID,
|
|
type='A',
|
|
name=full_domain,
|
|
content=ip_address,
|
|
ttl=1,
|
|
proxied=proxied
|
|
)
|
|
|
|
# Salva nel database
|
|
subdomain_id = Subdomain.create(
|
|
user_id=current_user.id,
|
|
subdomain=subdomain,
|
|
ip_address=ip_address,
|
|
vm_id=vm_id,
|
|
proxied=proxied,
|
|
cloudflare_record_id=record.id
|
|
)
|
|
|
|
log_user_action(vm_id or 0, 'subdomain_create', 'success', error_message=f'Sottodominio {full_domain} creato')
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': f'Sottodominio {full_domain} creato con successo!',
|
|
'subdomain_id': subdomain_id,
|
|
'full_domain': full_domain
|
|
})
|
|
|
|
except cloudflare.APIError as e:
|
|
app.logger.error(f"Errore Cloudflare: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': f'Errore Cloudflare: {str(e)}'}), 500
|
|
except Exception as e:
|
|
app.logger.error(f"Errore create_subdomain: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/subdomain/<int:subdomain_id>/delete', methods=['DELETE'])
|
|
@login_required
|
|
def delete_subdomain(subdomain_id):
|
|
"""Elimina un sottodominio"""
|
|
try:
|
|
# Recupera il sottodominio
|
|
subdomain_data = Subdomain.get_by_id(subdomain_id)
|
|
|
|
if not subdomain_data:
|
|
return jsonify({'status': 'error', 'message': 'Sottodominio non trovato'}), 404
|
|
|
|
# Verifica ownership
|
|
if subdomain_data['user_id'] != current_user.id and not current_user.is_admin:
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
# Elimina da Cloudflare
|
|
if subdomain_data['cloudflare_record_id'] and Config.CLOUDFLARE_API_TOKEN:
|
|
try:
|
|
import cloudflare
|
|
cf = cloudflare.Cloudflare(api_token=Config.CLOUDFLARE_API_TOKEN)
|
|
cf.dns.records.delete(
|
|
zone_id=Config.CLOUDFLARE_ZONE_ID,
|
|
dns_record_id=subdomain_data['cloudflare_record_id']
|
|
)
|
|
except cloudflare.APIError as e:
|
|
app.logger.warning(f"Errore eliminazione Cloudflare: {e}")
|
|
|
|
# Elimina dal database
|
|
if Subdomain.delete(subdomain_id, current_user.id if not current_user.is_admin else subdomain_data['user_id']):
|
|
log_user_action(
|
|
subdomain_data.get('vm_id', 0) or 0,
|
|
'subdomain_delete',
|
|
'success',
|
|
error_message=f'Sottodominio {subdomain_data["subdomain"]} eliminato'
|
|
)
|
|
return jsonify({'status': 'success', 'message': 'Sottodominio eliminato con successo'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'Errore durante eliminazione'}), 500
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore delete_subdomain: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/subdomain/<int:subdomain_id>/update-ip', methods=['PUT'])
|
|
@login_required
|
|
def update_subdomain_ip(subdomain_id):
|
|
"""Aggiorna l'IP di un sottodominio"""
|
|
try:
|
|
data = request.get_json()
|
|
new_ip = data.get('ip_address', '').strip()
|
|
|
|
if not new_ip:
|
|
return jsonify({'status': 'error', 'message': 'IP richiesto'}), 400
|
|
|
|
subdomain_data = Subdomain.get_by_id(subdomain_id)
|
|
|
|
if not subdomain_data:
|
|
return jsonify({'status': 'error', 'message': 'Sottodominio non trovato'}), 404
|
|
|
|
if subdomain_data['user_id'] != current_user.id and not current_user.is_admin:
|
|
return jsonify({'status': 'error', 'message': 'Accesso negato'}), 403
|
|
|
|
# Aggiorna su Cloudflare
|
|
if subdomain_data['cloudflare_record_id'] and Config.CLOUDFLARE_API_TOKEN:
|
|
try:
|
|
import cloudflare
|
|
cf = cloudflare.Cloudflare(api_token=Config.CLOUDFLARE_API_TOKEN)
|
|
full_domain = f"{subdomain_data['subdomain']}.{Config.CLOUDFLARE_DOMAIN}"
|
|
|
|
cf.dns.records.update(
|
|
zone_id=Config.CLOUDFLARE_ZONE_ID,
|
|
dns_record_id=subdomain_data['cloudflare_record_id'],
|
|
type='A',
|
|
name=full_domain,
|
|
content=new_ip,
|
|
ttl=1,
|
|
proxied=subdomain_data['proxied']
|
|
)
|
|
except cloudflare.APIError as e:
|
|
return jsonify({'status': 'error', 'message': f'Errore Cloudflare: {str(e)}'}), 500
|
|
|
|
# Aggiorna nel database
|
|
if Subdomain.update_ip(subdomain_id, current_user.id if not current_user.is_admin else subdomain_data['user_id'], new_ip):
|
|
return jsonify({'status': 'success', 'message': 'IP aggiornato con successo'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'Errore durante aggiornamento'}), 500
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore update_subdomain_ip: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
# ==================== ADMIN ROUTES ====================
|
|
|
|
@app.route('/admin/users')
|
|
@login_required
|
|
@admin_required
|
|
def admin_users():
|
|
"""Pannello admin - gestione utenti"""
|
|
return render_template('admin_users.html', user=current_user)
|
|
|
|
@app.route('/api/admin/users')
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_get_users():
|
|
"""API admin - lista utenti"""
|
|
try:
|
|
users = User.get_all_users()
|
|
return jsonify({'status': 'success', 'data': users})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/user/<int:user_id>/vms')
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_get_user_vms(user_id):
|
|
"""API admin - VM di un utente"""
|
|
try:
|
|
vms = UserVM.get_user_vms(user_id)
|
|
return jsonify({'status': 'success', 'data': vms})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/user/<int:user_id>/assign-vm', methods=['POST'])
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_assign_vm(user_id):
|
|
"""API admin - assegna VM a utente"""
|
|
try:
|
|
data = request.get_json()
|
|
vm_id = data.get('vm_id')
|
|
vm_name = data.get('vm_name')
|
|
notes = data.get('notes')
|
|
|
|
if not vm_id:
|
|
return jsonify({'status': 'error', 'message': 'VM ID richiesto'}), 400
|
|
|
|
UserVM.assign_vm(user_id, vm_id, vm_name, notes)
|
|
return jsonify({'status': 'success', 'message': 'VM assegnata con successo'})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/user/<int:user_id>/remove-vm/<int:vm_id>', methods=['DELETE'])
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_remove_vm(user_id, vm_id):
|
|
"""API admin - rimuove VM da utente"""
|
|
try:
|
|
UserVM.remove_vm(user_id, vm_id)
|
|
return jsonify({'status': 'success', 'message': 'VM rimossa con successo'})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/vm/<int:vm_id>/update-ip', methods=['PUT'])
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_update_vm_ip(vm_id):
|
|
"""API admin - aggiorna IP di una VM"""
|
|
try:
|
|
data = request.get_json()
|
|
ip_address = data.get('ip_address', '').strip()
|
|
|
|
conn = get_db_connection()
|
|
try:
|
|
with conn.cursor() as cursor:
|
|
sql = "UPDATE user_vms SET ip_address = %s WHERE vm_id = %s"
|
|
cursor.execute(sql, (ip_address if ip_address else None, vm_id))
|
|
return jsonify({'status': 'success', 'message': 'IP aggiornato con successo'})
|
|
finally:
|
|
conn.close()
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/user/create', methods=['POST'])
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_create_user():
|
|
"""API admin - crea nuovo utente"""
|
|
try:
|
|
data = request.get_json()
|
|
username = data.get('username')
|
|
email = data.get('email')
|
|
password = data.get('password')
|
|
is_admin = data.get('is_admin', False)
|
|
|
|
if not username or not email or not password:
|
|
return jsonify({'status': 'error', 'message': 'Username, email e password richiesti'}), 400
|
|
|
|
user_id = User.create(username, email, password, is_admin)
|
|
return jsonify({'status': 'success', 'message': 'Utente creato con successo', 'user_id': user_id})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/user/<int:user_id>/toggle-status', methods=['POST'])
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_toggle_user_status(user_id):
|
|
"""API admin - attiva/disattiva utente"""
|
|
try:
|
|
data = request.get_json()
|
|
active = data.get('active', True)
|
|
|
|
# Non permettere di disattivare se stesso
|
|
if user_id == current_user.id:
|
|
return jsonify({'status': 'error', 'message': 'Non puoi disattivare il tuo stesso account'}), 400
|
|
|
|
if User.update_active_status(user_id, active):
|
|
action = 'attivato' if active else 'disattivato'
|
|
return jsonify({'status': 'success', 'message': f'Utente {action} con successo'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'Errore durante aggiornamento'}), 500
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/logs')
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_get_logs():
|
|
"""API admin - recupera tutti i log"""
|
|
try:
|
|
limit = request.args.get('limit', 100, type=int)
|
|
logs = ActionLog.get_all_logs(limit)
|
|
return jsonify({'status': 'success', 'data': logs})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/stats')
|
|
@login_required
|
|
@admin_required
|
|
def api_admin_get_stats():
|
|
"""API admin - statistiche sistema"""
|
|
try:
|
|
# Conta utenti
|
|
users = User.get_all_users()
|
|
total_users = len(users)
|
|
|
|
# Conta VM totali e attive
|
|
all_vms = UserVM.get_all_assignments()
|
|
total_vms = len(set(vm['vm_id'] for vm in all_vms)) # VM uniche
|
|
|
|
# Conta VM attive controllando Proxmox
|
|
active_vms = 0
|
|
nodes_data, _ = proxmox_api_call('GET', '/cluster/resources')
|
|
if nodes_data and 'data' in nodes_data:
|
|
for resource in nodes_data['data']:
|
|
if resource.get('type') in ['qemu', 'lxc'] and resource.get('status') == 'running':
|
|
active_vms += 1
|
|
|
|
# Conta azioni oggi
|
|
logs = ActionLog.get_all_logs(1000)
|
|
today = datetime.now().date()
|
|
actions_today = sum(1 for log in logs if datetime.fromisoformat(str(log['created_at'])).date() == today)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'data': {
|
|
'total_users': total_users,
|
|
'total_vms': total_vms,
|
|
'active_vms': active_vms,
|
|
'actions_today': actions_today
|
|
}
|
|
})
|
|
except Exception as e:
|
|
app.logger.error(f"Errore api_admin_get_stats: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
# ==================== OVERVIEW & STATS ====================
|
|
|
|
@app.route('/overview')
|
|
@login_required
|
|
def overview():
|
|
"""Dashboard overview con statistiche aggregate"""
|
|
return render_template('overview.html', user=current_user)
|
|
|
|
@app.route('/api/overview/stats')
|
|
@login_required
|
|
def get_overview_stats():
|
|
"""Ottieni statistiche aggregate per tutte le VM dell'utente (o tutte se admin)"""
|
|
try:
|
|
# Se admin, mostra TUTTE le VM, altrimenti solo le VM dell'utente
|
|
if current_user.is_admin:
|
|
# Recupera tutte le VM dal cluster
|
|
resources_data, _ = proxmox_api_call('GET', '/cluster/resources')
|
|
if resources_data and 'data' in resources_data:
|
|
all_resources = [r for r in resources_data['data'] if r.get('type') in ['qemu', 'lxc']]
|
|
user_vms = [{'vm_id': r['vmid'], 'vm_name': r.get('name', f"VM-{r['vmid']}")} for r in all_resources]
|
|
else:
|
|
user_vms = []
|
|
else:
|
|
user_vms = UserVM.get_user_vms(current_user.id)
|
|
|
|
stats = {
|
|
'total_vms': len(user_vms),
|
|
'running_vms': 0,
|
|
'stopped_vms': 0,
|
|
'total_cpu_usage': 0,
|
|
'total_memory_used': 0,
|
|
'total_memory_max': 0,
|
|
'total_disk_used': 0,
|
|
'total_disk_max': 0,
|
|
'vms': []
|
|
}
|
|
|
|
nodes_data, _ = proxmox_api_call('GET', '/nodes')
|
|
if not nodes_data or 'data' not in nodes_data:
|
|
return jsonify({'status': 'error', 'message': 'Impossibile contattare Proxmox'}), 500
|
|
|
|
for vm in user_vms:
|
|
vm_id = vm['vm_id']
|
|
vm_status = None
|
|
|
|
for node in nodes_data.get('data', []):
|
|
node_name = node['node']
|
|
|
|
# Prova QEMU
|
|
qemu_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/qemu/{vm_id}/status/current')
|
|
if qemu_data and 'data' in qemu_data:
|
|
vm_status = qemu_data['data']
|
|
vm_status['node'] = node_name
|
|
break
|
|
|
|
# Prova LXC
|
|
lxc_data, _ = proxmox_api_call('GET', f'/nodes/{node_name}/lxc/{vm_id}/status/current')
|
|
if lxc_data and 'data' in lxc_data:
|
|
vm_status = lxc_data['data']
|
|
vm_status['node'] = node_name
|
|
break
|
|
|
|
if vm_status:
|
|
if vm_status.get('status') == 'running':
|
|
stats['running_vms'] += 1
|
|
stats['total_cpu_usage'] += vm_status.get('cpu', 0)
|
|
stats['total_memory_used'] += vm_status.get('mem', 0)
|
|
else:
|
|
stats['stopped_vms'] += 1
|
|
|
|
stats['total_memory_max'] += vm_status.get('maxmem', 0)
|
|
stats['total_disk_max'] += vm_status.get('maxdisk', 0)
|
|
|
|
mem_percent = 0
|
|
if vm_status.get('maxmem', 0) > 0:
|
|
mem_percent = round((vm_status.get('mem', 0) / vm_status.get('maxmem', 1)) * 100, 1)
|
|
|
|
stats['vms'].append({
|
|
'vm_id': vm_id,
|
|
'name': vm['vm_name'] or vm_status.get('name', f'VM-{vm_id}'),
|
|
'status': vm_status.get('status', 'unknown'),
|
|
'cpu': vm_status.get('cpu', 0),
|
|
'memory_percent': mem_percent
|
|
})
|
|
|
|
# Calcola medie
|
|
if stats['running_vms'] > 0:
|
|
stats['avg_cpu_usage'] = round((stats['total_cpu_usage'] / stats['running_vms']) * 100, 1)
|
|
else:
|
|
stats['avg_cpu_usage'] = 0
|
|
|
|
if stats['total_memory_max'] > 0:
|
|
stats['memory_percent'] = round((stats['total_memory_used'] / stats['total_memory_max']) * 100, 1)
|
|
else:
|
|
stats['memory_percent'] = 0
|
|
|
|
return jsonify({'status': 'success', 'data': stats})
|
|
|
|
except Exception as e:
|
|
app.logger.error(f"Errore get_overview_stats: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
# ==================== NOTIFICATIONS (SSE) ====================
|
|
|
|
def get_user_queue(user_id):
|
|
"""Ottieni o crea la coda di notifiche per un utente"""
|
|
if user_id not in notification_queues:
|
|
notification_queues[user_id] = queue.Queue(maxsize=100)
|
|
return notification_queues[user_id]
|
|
|
|
def send_notification(user_id, notification_type, message, data=None):
|
|
"""Invia una notifica a un utente specifico"""
|
|
try:
|
|
q = get_user_queue(user_id)
|
|
notification = {
|
|
'type': 'notification',
|
|
'notification_type': notification_type,
|
|
'message': message,
|
|
'data': data or {},
|
|
'timestamp': time.time()
|
|
}
|
|
# Non bloccare se la coda è piena
|
|
try:
|
|
q.put_nowait(notification)
|
|
except queue.Full:
|
|
pass
|
|
except Exception as e:
|
|
app.logger.error(f"Errore send_notification: {e}")
|
|
|
|
@app.route('/api/notifications/stream')
|
|
@login_required
|
|
def notification_stream():
|
|
"""Endpoint SSE per notifiche real-time"""
|
|
def generate():
|
|
q = get_user_queue(current_user.id)
|
|
|
|
while True:
|
|
try:
|
|
# Attendi notifica con timeout
|
|
notification = q.get(timeout=30)
|
|
yield f"data: {json.dumps(notification)}\n\n"
|
|
except queue.Empty:
|
|
# Invia ping keepalive
|
|
yield f"data: {json.dumps({'type': 'ping'})}\n\n"
|
|
|
|
return Response(
|
|
generate(),
|
|
mimetype='text/event-stream',
|
|
headers={
|
|
'Cache-Control': 'no-cache',
|
|
'X-Accel-Buffering': 'no',
|
|
'Connection': 'keep-alive'
|
|
}
|
|
)
|
|
|
|
# ==================== NUOVE FUNZIONALITÀ ====================
|
|
|
|
# Log di Sistema
|
|
@app.route('/admin/system-logs')
|
|
@login_required
|
|
@admin_required
|
|
def system_logs():
|
|
"""Pagina log di sistema completa"""
|
|
return render_template('system_logs.html', user=current_user)
|
|
|
|
# Topologia Cluster
|
|
@app.route('/admin/cluster-topology')
|
|
@login_required
|
|
@admin_required
|
|
def cluster_topology():
|
|
"""Pagina topologia cluster"""
|
|
return render_template('cluster_topology.html', user=current_user)
|
|
|
|
@app.route('/api/admin/cluster/topology')
|
|
@login_required
|
|
@admin_required
|
|
def api_cluster_topology():
|
|
"""API per ottenere la topologia del cluster"""
|
|
try:
|
|
# Recupera informazioni sui nodi
|
|
nodes_data, error = proxmox_api_call('GET', '/nodes')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
# Recupera tutte le risorse del cluster
|
|
resources_data, error = proxmox_api_call('GET', '/cluster/resources')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
topology = {
|
|
'nodes': nodes_data.get('data', []),
|
|
'resources': resources_data.get('data', [])
|
|
}
|
|
|
|
return jsonify({'status': 'success', 'data': topology})
|
|
except Exception as e:
|
|
app.logger.error(f"Errore api_cluster_topology: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
# IPAM (IP Address Management)
|
|
@app.route('/admin/ipam')
|
|
@login_required
|
|
@admin_required
|
|
def ipam():
|
|
"""Pagina gestione IPAM"""
|
|
return render_template('ipam.html', user=current_user)
|
|
|
|
@app.route('/api/admin/ipam')
|
|
@login_required
|
|
@admin_required
|
|
def api_ipam():
|
|
"""API per ottenere tutti gli IP delle VM/LXC"""
|
|
try:
|
|
ipam_data = []
|
|
|
|
# Recupera assegnazioni manuali dal database
|
|
manual_assignments = {item['vm_id']: item for item in IPAssignment.get_all()}
|
|
|
|
# Recupera tutte le risorse
|
|
resources_data, error = proxmox_api_call('GET', '/cluster/resources')
|
|
if error:
|
|
return jsonify({'status': 'error', 'message': error}), 500
|
|
|
|
# Filtra solo VM e LXC
|
|
resources = [r for r in resources_data.get('data', []) if r.get('type') in ['qemu', 'lxc']]
|
|
|
|
for resource in resources:
|
|
vmid = resource.get('vmid')
|
|
vm_type = resource.get('type')
|
|
node = resource.get('node')
|
|
|
|
# Controlla se c'è un'assegnazione manuale
|
|
manual = manual_assignments.get(vmid)
|
|
|
|
if manual:
|
|
# Usa l'IP assegnato manualmente
|
|
ip_address = manual['ip_address']
|
|
mac_address = manual['mac_address']
|
|
notes = manual['notes']
|
|
else:
|
|
# Recupera IP automaticamente
|
|
notes = None
|
|
if vm_type == 'qemu':
|
|
config_data, _ = proxmox_api_call('GET', f'/nodes/{node}/qemu/{vmid}/agent/network-get-interfaces')
|
|
ip_address = 'N/A'
|
|
mac_address = None
|
|
|
|
if config_data and 'data' in config_data:
|
|
result = config_data['data'].get('result', [])
|
|
for interface in result:
|
|
if interface.get('name') not in ['lo', 'sit0']:
|
|
ip_addresses = interface.get('ip-addresses', [])
|
|
for ip_info in ip_addresses:
|
|
if ip_info.get('ip-address-type') == 'ipv4':
|
|
ip_address = ip_info.get('ip-address', 'N/A')
|
|
break
|
|
mac_address = interface.get('hardware-address')
|
|
if ip_address != 'N/A':
|
|
break
|
|
else: # LXC
|
|
config_data, _ = proxmox_api_call('GET', f'/nodes/{node}/lxc/{vmid}/config')
|
|
ip_address = 'N/A'
|
|
mac_address = None
|
|
|
|
if config_data and 'data' in config_data:
|
|
# Cerca net0, net1, etc.
|
|
for key in config_data['data']:
|
|
if key.startswith('net'):
|
|
net_config = config_data['data'][key]
|
|
# Formato: name=eth0,bridge=vmbr0,hwaddr=XX:XX:XX:XX:XX:XX,ip=192.168.1.100/24
|
|
if 'ip=' in net_config:
|
|
ip_part = net_config.split('ip=')[1].split(',')[0]
|
|
ip_address = ip_part.split('/')[0] if '/' in ip_part else ip_part
|
|
if 'hwaddr=' in net_config:
|
|
mac_address = net_config.split('hwaddr=')[1].split(',')[0]
|
|
break
|
|
|
|
# Controlla conflitti IP
|
|
conflict = False
|
|
if ip_address != 'N/A':
|
|
conflict = sum(1 for item in ipam_data if item['ip'] == ip_address) > 0
|
|
|
|
ipam_data.append({
|
|
'vmid': vmid,
|
|
'name': resource.get('name', f'{vm_type}-{vmid}'),
|
|
'type': vm_type,
|
|
'ip': ip_address,
|
|
'mac': mac_address,
|
|
'node': node,
|
|
'status': resource.get('status', 'unknown'),
|
|
'conflict': conflict,
|
|
'notes': notes,
|
|
'manual_assignment': manual is not None
|
|
})
|
|
|
|
return jsonify({'status': 'success', 'data': ipam_data})
|
|
except Exception as e:
|
|
app.logger.error(f"Errore api_ipam: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/ipam/scan', methods=['POST'])
|
|
@login_required
|
|
@admin_required
|
|
def api_ipam_scan():
|
|
"""Scansiona la rete per aggiornare gli IP"""
|
|
try:
|
|
# In una implementazione reale, qui si eseguirebbe una scansione di rete
|
|
# Per ora ricarica semplicemente i dati
|
|
return jsonify({'status': 'success', 'message': 'Scansione completata'})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/ipam/assign', methods=['POST'])
|
|
@login_required
|
|
@admin_required
|
|
def api_ipam_assign():
|
|
"""Assegna manualmente un IP a una VM"""
|
|
try:
|
|
data = request.get_json()
|
|
vmid = data.get('vmid')
|
|
ip = data.get('ip')
|
|
notes = data.get('notes', '')
|
|
|
|
if not vmid or not ip:
|
|
return jsonify({'status': 'error', 'message': 'VM ID e IP sono obbligatori'}), 400
|
|
|
|
# Valida formato IP
|
|
import re
|
|
ip_pattern = re.compile(r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$')
|
|
if not ip_pattern.match(ip):
|
|
return jsonify({'status': 'error', 'message': 'Formato IP non valido'}), 400
|
|
|
|
# Controlla conflitti IP
|
|
conflicting_vm = IPAssignment.check_ip_conflict(ip, exclude_vm_id=vmid)
|
|
if conflicting_vm:
|
|
return jsonify({
|
|
'status': 'error',
|
|
'message': f'IP già assegnato alla VM {conflicting_vm}'
|
|
}), 409
|
|
|
|
# Salva assegnazione nel database
|
|
IPAssignment.create(
|
|
vm_id=vmid,
|
|
ip_address=ip,
|
|
assigned_by=current_user.id,
|
|
notes=notes
|
|
)
|
|
|
|
# Log azione
|
|
ActionLog.log_action(
|
|
user_id=current_user.id,
|
|
vm_id=vmid,
|
|
action_type='ip_assign',
|
|
status='success',
|
|
ip_address=request.remote_addr
|
|
)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': f'IP {ip} assegnato a VM {vmid}',
|
|
'data': {'vmid': vmid, 'ip': ip, 'notes': notes}
|
|
})
|
|
except Exception as e:
|
|
app.logger.error(f"Errore api_ipam_assign: {traceback.format_exc()}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
@app.route('/api/admin/ipam/ping', methods=['POST'])
|
|
@login_required
|
|
@admin_required
|
|
def api_ipam_ping():
|
|
"""Esegue un ping verso un IP"""
|
|
try:
|
|
data = request.get_json()
|
|
ip = data.get('ip')
|
|
|
|
if not ip:
|
|
return jsonify({'status': 'error', 'message': 'IP richiesto'}), 400
|
|
|
|
# Esegue ping (Windows usa -n, Linux usa -c)
|
|
import platform
|
|
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
|
command = f'ping {param} 1 {ip}'
|
|
|
|
import subprocess
|
|
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=5)
|
|
|
|
if result.returncode == 0:
|
|
return jsonify({'status': 'success', 'message': f'{ip} raggiungibile'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': f'{ip} non raggiungibile'}), 400
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return jsonify({'status': 'error', 'message': 'Timeout ping'}), 408
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
|
|
# ==================== ERROR HANDLERS ====================
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(e):
|
|
return render_template('404.html'), 404
|
|
|
|
@app.errorhandler(500)
|
|
def internal_error(e):
|
|
app.logger.error(f"Errore 500: {traceback.format_exc()}")
|
|
return render_template('500.html'), 500
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True, host='0.0.0.0', port=5000, threaded=True)
|