241 lines
10 KiB
Python
241 lines
10 KiB
Python
import time, json, os, threading, subprocess
|
|
from datetime import datetime
|
|
from flask import Flask, render_template, jsonify, request, redirect, url_for
|
|
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user
|
|
from pyModbusTCP.client import ModbusClient
|
|
from zabbix_utils import Sender, ItemValue
|
|
from cryptography.fernet import Fernet
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = 'voltec_master_final_ultra_v6'
|
|
|
|
# --- 1. DEFINIÇÕES GLOBAIS E PERSISTÊNCIA (Devem vir antes das rotas) ---
|
|
ARQUIVO_CONFIG = 'config_nodes.json'
|
|
ARQUIVO_MAPA = 'mapa_dispositivos.json'
|
|
ARQUIVO_LICENSA = 'license.key'
|
|
MASTER_KEY = b'vS-C5Z_R1ST-Gf_K8_L9_Xo2-M1A3B5C7D9E1F2G3H4='
|
|
|
|
def carregar_dados(arquivo, padrao):
|
|
if os.path.exists(arquivo):
|
|
try:
|
|
with open(arquivo, 'r') as f: return json.load(f)
|
|
except: return padrao
|
|
return padrao
|
|
|
|
def salvar_dados(arquivo, dados):
|
|
with open(arquivo, 'w') as f: json.dump(dados, f, indent=4)
|
|
|
|
# Carregamos a config aqui para o login não dar erro
|
|
config_global = carregar_dados(ARQUIVO_CONFIG, {
|
|
"zabbix_server": "172.16.33.6",
|
|
"hostname_zabbix": "NFS-320",
|
|
"web_user": "admin",
|
|
"web_password": "admin",
|
|
"nodes": []
|
|
})
|
|
mapa_dispositivos = carregar_dados(ARQUIVO_MAPA, {})
|
|
dados_tempo_real = {}
|
|
status_nodes_web = {}
|
|
deve_reiniciar = False
|
|
|
|
MAPA_STATUS_BASE = {
|
|
5120: {"label": "NORMAL / OK", "color": "#28a745"},
|
|
3088: {"label": "INCIDENTE", "color": "#dc3545"},
|
|
3344: {"label": "INCIDENTE ACK", "color": "#007bff"},
|
|
13312: {"label": "REMOVIDO", "color": "#fd7e14"},
|
|
0: {"label": "VAZIO", "color": "#6c757d"}
|
|
}
|
|
|
|
# --- 2. GESTÃO DE ACESSO ---
|
|
login_manager = LoginManager()
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = 'login'
|
|
|
|
class User(UserMixin):
|
|
def __init__(self, id): self.id = id
|
|
|
|
@login_manager.user_loader
|
|
def load_user(user_id): return User(user_id)
|
|
|
|
# --- 3. AUXILIARES E LICENCIAMENTO (VERSÃO COM DIAS) ---
|
|
def get_status_info(valor):
|
|
lsb = valor & 0xFF
|
|
return MAPA_STATUS_BASE.get(valor, MAPA_STATUS_BASE.get(lsb, {"label": f"ST {valor}", "color": "#6c757d"}))
|
|
|
|
def get_hardware_id():
|
|
try:
|
|
if os.name == 'nt':
|
|
cmd = 'powershell "(Get-CimInstance Win32_ComputerSystemProduct).UUID.Trim()"'
|
|
return subprocess.check_output(cmd, shell=True).decode().strip()
|
|
return subprocess.check_output('cat /etc/machine-id', shell=True).decode().strip()[:32]
|
|
except: return "ID-ERRO-VOLTEC-001"
|
|
|
|
def verificar_licenca():
|
|
if not os.path.exists(ARQUIVO_LICENSA):
|
|
return False, "Licença ausente", get_hardware_id(), 0
|
|
try:
|
|
f = Fernet(MASTER_KEY)
|
|
with open(ARQUIVO_LICENSA, 'r') as file:
|
|
serial = file.read().strip()
|
|
|
|
dados = json.loads(f.decrypt(serial.encode()).decode())
|
|
|
|
if dados['h'].strip() != get_hardware_id().strip():
|
|
return False, "HWID Incompatível", get_hardware_id(), 0
|
|
|
|
expira = datetime.strptime(dados['e'], '%Y-%m-%d')
|
|
dias_restantes = (expira - datetime.now()).days
|
|
|
|
if dias_restantes < 0:
|
|
return False, "Expirada", get_hardware_id(), 0
|
|
|
|
return True, f"Licenciado para {dados['c']}", get_hardware_id(), dias_restantes
|
|
except:
|
|
return False, "Serial Inválido", get_hardware_id(), 0
|
|
|
|
# --- 4. MOTOR MODBUS E BACKGROUND ---
|
|
def realizar_discovery_node(node, mapa_atual):
|
|
client = ModbusClient(host=node['ip'], port=502, unit_id=int(node['unit']), auto_open=True, timeout=0.5)
|
|
mapa_node = {}
|
|
for addr in range(1, 601):
|
|
res = client.read_holding_registers(addr, 1)
|
|
if res and res[0] not in [0, 255, 65535]:
|
|
key = f"{node['ip']}_{node['unit']}_{addr}"
|
|
if key in mapa_atual: label = mapa_atual[key]['label']
|
|
else:
|
|
tipo = 'D' if addr < 160 else ('M' if addr < 350 else 'P')
|
|
label = f"{node['nome']}-{tipo}{addr:03}"
|
|
mapa_node[key] = {"label": label, "status": res[0], "node_nome": node['nome']}
|
|
client.close()
|
|
return mapa_node
|
|
|
|
def monitorar_background():
|
|
global dados_tempo_real, deve_reiniciar, config_global, mapa_dispositivos, status_nodes_web
|
|
while True:
|
|
licenciado, _, _, _ = verificar_licenca()
|
|
if not licenciado:
|
|
status_nodes_web = {"SISTEMA": "BLOQUEADO"}
|
|
time.sleep(5); continue
|
|
|
|
config_global = carregar_dados(ARQUIVO_CONFIG, config_global)
|
|
mapa_dispositivos = carregar_dados(ARQUIVO_MAPA, mapa_dispositivos)
|
|
sender = Sender(server=config_global['zabbix_server'])
|
|
hostname = config_global['hostname_zabbix']
|
|
|
|
# Discovery LLD (Zabbix re-cadastra nomes se houver mudança)
|
|
if mapa_dispositivos:
|
|
lld = {"data": []}
|
|
for k, info in mapa_dispositivos.items(): lld["data"].append({"{#PONTO_LABEL}": info["label"]})
|
|
for n in config_global['nodes']: lld["data"].append({"{#NODE_NOME}": n["nome"]})
|
|
try: sender.send([ItemValue(hostname, "notifier.discovery", json.dumps(lld))])
|
|
except: pass
|
|
|
|
while not deve_reiniciar:
|
|
metrics = []
|
|
for node in config_global['nodes']:
|
|
c = ModbusClient(host=node['ip'], port=502, unit_id=int(node['unit']), auto_open=True, timeout=1)
|
|
res = c.read_holding_registers(1, 1)
|
|
status_conn = 1 if res is not None else 0
|
|
status_nodes_web[node['nome']] = "Online" if status_conn == 1 else "Offline"
|
|
metrics.append(ItemValue(hostname, f'node.status[{node["nome"]}]', status_conn))
|
|
if status_conn == 1:
|
|
for key, info in list(mapa_dispositivos.items()):
|
|
if key.startswith(f"{node['ip']}_{node['unit']}_"):
|
|
addr = int(key.split('_')[-1])
|
|
r = c.read_holding_registers(addr, 1)
|
|
if r:
|
|
val = r[0]
|
|
dados_tempo_real[key] = {"label": info['label'], "status": val, "node": node['nome'], "last": time.strftime('%H:%M:%S')}
|
|
metrics.append(ItemValue(hostname, f'notifier.status[{info["label"]}]', val & 0xFF))
|
|
metrics.append(ItemValue(hostname, f'notifier.comm[{info["label"]}]', (val >> 8) & 0xFF))
|
|
c.close()
|
|
if metrics:
|
|
try: sender.send(metrics)
|
|
except: pass
|
|
time.sleep(2)
|
|
deve_reiniciar = False
|
|
|
|
# --- 5. ROTAS ---
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
if request.method == 'POST':
|
|
# Aqui o config_global agora existe!
|
|
if request.form.get('username') == config_global['web_user'] and \
|
|
request.form.get('password') == config_global['web_password']:
|
|
login_user(User(config_global['web_user']))
|
|
return redirect(url_for('index'))
|
|
return render_template('login.html')
|
|
|
|
@app.route('/')
|
|
@login_required
|
|
def index():
|
|
# Agora recebemos 4 valores da função
|
|
lic, msg, hwid, dias = verificar_licenca()
|
|
return render_template('index.html',
|
|
config=config_global,
|
|
licenciado=lic,
|
|
lic_msg=msg,
|
|
hwid=hwid,
|
|
dias_restantes=dias)
|
|
|
|
@app.route('/api/nodes', methods=['POST'])
|
|
@login_required
|
|
def api_nodes():
|
|
global deve_reiniciar, config_global, mapa_dispositivos
|
|
data = request.json
|
|
config_global.update({'nodes': data['nodes'], 'zabbix_server': data['zabbix_server'], 'hostname_zabbix': data['hostname_zabbix']})
|
|
salvar_dados(ARQUIVO_CONFIG, config_global)
|
|
ips_ativos = [f"{n['ip']}_{n['unit']}_" for n in data['nodes']]
|
|
mapa_dispositivos = {k: v for k, v in mapa_dispositivos.items() if any(k.startswith(p) for p in ips_ativos)}
|
|
salvar_dados(ARQUIVO_MAPA, mapa_dispositivos)
|
|
deve_reiniciar = True
|
|
return jsonify({"status": "success"})
|
|
|
|
@app.route('/api/rename', methods=['POST'])
|
|
@login_required
|
|
def api_rename():
|
|
global deve_reiniciar
|
|
data = request.json
|
|
if data['addr'] in mapa_dispositivos:
|
|
mapa_dispositivos[data['addr']]['label'] = data['label']
|
|
salvar_dados(ARQUIVO_MAPA, mapa_dispositivos)
|
|
deve_reiniciar = True # Força envio do novo nome ao Zabbix
|
|
return jsonify({"status": "success"})
|
|
return jsonify({"status": "error"}), 404
|
|
|
|
@app.route('/api/license', methods=['POST'])
|
|
def api_license():
|
|
global deve_reiniciar
|
|
with open(ARQUIVO_LICENSA, 'w') as f: f.write(request.json['serial'].strip())
|
|
deve_reiniciar = True
|
|
return jsonify({"status": "success"})
|
|
|
|
@app.route('/api/rescan_node', methods=['POST'])
|
|
@login_required
|
|
def api_rescan():
|
|
global deve_reiniciar, mapa_dispositivos
|
|
node_data = request.json
|
|
res_mapa = realizar_discovery_node(node_data, mapa_dispositivos)
|
|
prefixo = f"{node_data['ip']}_{node_data['unit']}_"
|
|
mapa_dispositivos = {k: v for k, v in mapa_dispositivos.items() if not k.startswith(prefixo)}
|
|
mapa_dispositivos.update(res_mapa)
|
|
salvar_dados(ARQUIVO_MAPA, mapa_dispositivos)
|
|
deve_reiniciar = True
|
|
return jsonify({"status": "success"})
|
|
|
|
@app.route('/api/data')
|
|
@login_required
|
|
def api_data():
|
|
lista = []
|
|
for key, info in mapa_dispositivos.items():
|
|
tr = dados_tempo_real.get(key, {"status": 0, "last": "--:--:--", "node": info.get('node_nome', '...')})
|
|
st_info = get_status_info(tr['status'])
|
|
lista.append({"key": key, "label": info['label'], "node": tr.get('node', '...'), "status_text": st_info['label'], "color": st_info['color'], "last": tr['last']})
|
|
return jsonify({"sensores": lista, "nodes_status": status_nodes_web})
|
|
|
|
@app.route('/logout')
|
|
def logout(): logout_user(); return redirect(url_for('login'))
|
|
|
|
if __name__ == "__main__":
|
|
threading.Thread(target=monitorar_background, daemon=True).start()
|
|
app.run(host='0.0.0.0', port=8080) |