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)