Software Recriado atravez do codex
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask
|
||||
from flask_login import LoginManager
|
||||
|
||||
from .core import (
|
||||
LicenseService,
|
||||
MonitorEngine,
|
||||
RuntimeState,
|
||||
User,
|
||||
ensure_secret_key,
|
||||
load_license_key,
|
||||
)
|
||||
from .routes import register_routes
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
logging.basicConfig(
|
||||
level=os.getenv("VFM_LOG_LEVEL", "INFO").upper(),
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
)
|
||||
|
||||
|
||||
def is_production_mode() -> bool:
|
||||
value = os.getenv("VFM_ENV", "").strip().lower()
|
||||
return value in {"prod", "production"}
|
||||
|
||||
|
||||
def create_app(base_dir: str | Path | None = None, start_monitor: bool = True) -> Flask:
|
||||
configure_logging()
|
||||
|
||||
resolved_base_dir = Path(base_dir) if base_dir else Path(__file__).resolve().parent.parent
|
||||
secret_path = resolved_base_dir / "app_secret.key"
|
||||
default_password = os.getenv("VFM_DEFAULT_PASSWORD", "admin")
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
template_folder=str(resolved_base_dir / "templates"),
|
||||
)
|
||||
app.secret_key = os.getenv("VFM_APP_SECRET") or ensure_secret_key(secret_path)
|
||||
|
||||
runtime_state = RuntimeState(resolved_base_dir, default_password)
|
||||
license_service = LicenseService(
|
||||
resolved_base_dir / "license.key",
|
||||
load_license_key(require_env=is_production_mode()),
|
||||
)
|
||||
|
||||
app.extensions["runtime_state"] = runtime_state
|
||||
app.extensions["license_service"] = license_service
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "login"
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User(user_id)
|
||||
|
||||
register_routes(app)
|
||||
|
||||
if start_monitor:
|
||||
monitor_engine = MonitorEngine(runtime_state, license_service)
|
||||
app.extensions["monitor_engine"] = monitor_engine
|
||||
monitor_engine.start()
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,494 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from pyModbusTCP.client import ModbusClient
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
from zabbix_utils import ItemValue, Sender
|
||||
|
||||
|
||||
DEFAULT_CONFIG_TEMPLATE = {
|
||||
"zabbix_server": "172.16.33.6",
|
||||
"hostname_zabbix": "NFS-320",
|
||||
"web_user": "admin",
|
||||
"nodes": [],
|
||||
}
|
||||
LEGACY_LICENSE_KEY = b"vS-C5Z_R1ST-Gf_K8_L9_Xo2-M1A3B5C7D9E1F2G3H4="
|
||||
MAX_DISCOVERY_ADDR = 600
|
||||
MODBUS_BLOCK_SIZE = 120
|
||||
DISCOVERY_IGNORED_VALUES = {0, 255, 65535}
|
||||
POLL_INTERVAL_SECONDS = 2
|
||||
RETRY_INTERVAL_SECONDS = 5
|
||||
|
||||
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"},
|
||||
}
|
||||
|
||||
LOGGER = logging.getLogger("v-fire-monitor")
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(self, user_id: str):
|
||||
self.id = user_id
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_anonymous(self) -> bool:
|
||||
return False
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
|
||||
class JsonStore:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
|
||||
def load(self, default):
|
||||
if not self.path.exists():
|
||||
return default
|
||||
try:
|
||||
with self.path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
LOGGER.warning("Falha ao carregar %s: %s", self.path.name, exc)
|
||||
return default
|
||||
|
||||
def save(self, data) -> None:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_path = self.path.with_suffix(f"{self.path.suffix}.tmp")
|
||||
with temp_path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(data, handle, indent=4, ensure_ascii=False)
|
||||
temp_path.replace(self.path)
|
||||
|
||||
|
||||
def build_default_config(default_password: str) -> dict:
|
||||
config = dict(DEFAULT_CONFIG_TEMPLATE)
|
||||
config["web_password_hash"] = generate_password_hash(default_password)
|
||||
return config
|
||||
|
||||
|
||||
def ensure_secret_key(secret_path: Path) -> str:
|
||||
if secret_path.exists():
|
||||
return secret_path.read_text(encoding="utf-8").strip()
|
||||
secret = Fernet.generate_key().decode("ascii")
|
||||
secret_path.write_text(secret, encoding="utf-8")
|
||||
LOGGER.info("Chave de sessão criada em %s", secret_path.name)
|
||||
return secret
|
||||
|
||||
|
||||
def normalize_config(data: dict, default_password: str) -> dict:
|
||||
config = build_default_config(default_password)
|
||||
config.update(data or {})
|
||||
|
||||
if config.get("web_password") and not config.get("web_password_hash"):
|
||||
config["web_password_hash"] = generate_password_hash(config["web_password"])
|
||||
|
||||
config.pop("web_password", None)
|
||||
|
||||
normalized_nodes = []
|
||||
for raw_node in config.get("nodes", []):
|
||||
try:
|
||||
normalized_nodes.append(validate_node_payload(raw_node))
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("Node ignorado durante normalizacao: %s", exc)
|
||||
config["nodes"] = normalized_nodes
|
||||
return config
|
||||
|
||||
|
||||
def load_license_key(require_env: bool) -> bytes:
|
||||
env_key = os.getenv("VFM_LICENSE_MASTER_KEY")
|
||||
if env_key:
|
||||
return env_key.encode("utf-8")
|
||||
if require_env:
|
||||
raise RuntimeError(
|
||||
"VFM_LICENSE_MASTER_KEY e obrigatoria em producao. Defina a variavel de ambiente antes de iniciar o sistema."
|
||||
)
|
||||
LOGGER.warning(
|
||||
"VFM_LICENSE_MASTER_KEY nao definido. Usando chave legada embutida; mova a chave para variavel de ambiente."
|
||||
)
|
||||
return LEGACY_LICENSE_KEY
|
||||
|
||||
|
||||
def get_status_info(value: int) -> dict:
|
||||
low_byte = value & 0xFF
|
||||
return MAPA_STATUS_BASE.get(
|
||||
value,
|
||||
MAPA_STATUS_BASE.get(low_byte, {"label": f"ST {value}", "color": "#6c757d"}),
|
||||
)
|
||||
|
||||
|
||||
def get_hardware_id() -> str:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
import subprocess
|
||||
|
||||
output = subprocess.check_output(
|
||||
[
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"(Get-CimInstance Win32_ComputerSystemProduct).UUID.Trim()",
|
||||
],
|
||||
text=True,
|
||||
)
|
||||
return output.strip()
|
||||
|
||||
machine_id = Path("/etc/machine-id")
|
||||
if machine_id.exists():
|
||||
return machine_id.read_text(encoding="utf-8").strip()[:32]
|
||||
except Exception as exc: # pragma: no cover
|
||||
LOGGER.warning("Nao foi possivel obter hardware id: %s", exc)
|
||||
return "ID-ERRO-VOLTEC-001"
|
||||
|
||||
|
||||
def validate_node_payload(data: dict) -> dict:
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("Formato de node invalido.")
|
||||
|
||||
nome = str(data.get("nome", "")).strip()
|
||||
ip = str(data.get("ip", "")).strip()
|
||||
|
||||
if not nome:
|
||||
raise ValueError("Nome do node e obrigatorio.")
|
||||
if not ip:
|
||||
raise ValueError("IP do node e obrigatorio.")
|
||||
|
||||
try:
|
||||
unit = int(data.get("unit"))
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f"Unit invalido para node {nome}.") from None
|
||||
|
||||
if unit < 0 or unit > 255:
|
||||
raise ValueError(f"Unit fora do intervalo para node {nome}.")
|
||||
|
||||
return {"nome": nome, "ip": ip, "unit": unit}
|
||||
|
||||
|
||||
def contiguous_ranges(addresses: List[int]) -> List[Tuple[int, int]]:
|
||||
if not addresses:
|
||||
return []
|
||||
|
||||
sorted_addresses = sorted(set(addresses))
|
||||
ranges: List[Tuple[int, int]] = []
|
||||
start = sorted_addresses[0]
|
||||
previous = start
|
||||
|
||||
for address in sorted_addresses[1:]:
|
||||
if address == previous + 1 and (address - start + 1) <= MODBUS_BLOCK_SIZE:
|
||||
previous = address
|
||||
continue
|
||||
ranges.append((start, previous))
|
||||
start = previous = address
|
||||
ranges.append((start, previous))
|
||||
return ranges
|
||||
|
||||
|
||||
def discover_node(node: dict, current_map: dict) -> dict:
|
||||
client = ModbusClient(
|
||||
host=node["ip"],
|
||||
port=502,
|
||||
unit_id=node["unit"],
|
||||
auto_open=True,
|
||||
timeout=1,
|
||||
)
|
||||
discovered = {}
|
||||
|
||||
try:
|
||||
for start in range(1, MAX_DISCOVERY_ADDR + 1, MODBUS_BLOCK_SIZE):
|
||||
count = min(MODBUS_BLOCK_SIZE, MAX_DISCOVERY_ADDR - start + 1)
|
||||
registers = client.read_holding_registers(start, count)
|
||||
if not registers:
|
||||
continue
|
||||
|
||||
for offset, value in enumerate(registers):
|
||||
if value in DISCOVERY_IGNORED_VALUES:
|
||||
continue
|
||||
address = start + offset
|
||||
key = f"{node['ip']}_{node['unit']}_{address}"
|
||||
existing = current_map.get(key, {})
|
||||
if existing.get("label"):
|
||||
label = existing["label"]
|
||||
else:
|
||||
device_type = "D" if address < 160 else ("M" if address < 350 else "P")
|
||||
label = f"{node['nome']}-{device_type}{address:03}"
|
||||
discovered[key] = {
|
||||
"label": label,
|
||||
"status": value,
|
||||
"node_nome": node["nome"],
|
||||
}
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return discovered
|
||||
|
||||
|
||||
@dataclass
|
||||
class LicenseStatus:
|
||||
valid: bool
|
||||
message: str
|
||||
hardware_id: str
|
||||
days_remaining: int
|
||||
|
||||
|
||||
class LicenseService:
|
||||
def __init__(self, license_path: Path, master_key: bytes):
|
||||
self.license_path = license_path
|
||||
self.fernet = Fernet(master_key)
|
||||
|
||||
def verify(self) -> LicenseStatus:
|
||||
hardware_id = get_hardware_id()
|
||||
if not self.license_path.exists():
|
||||
return LicenseStatus(False, "Licenca ausente", hardware_id, 0)
|
||||
|
||||
try:
|
||||
serial = self.license_path.read_text(encoding="utf-8").strip()
|
||||
payload = json.loads(self.fernet.decrypt(serial.encode("utf-8")).decode("utf-8"))
|
||||
licensed_hwid = str(payload["h"]).strip()
|
||||
if licensed_hwid != hardware_id.strip():
|
||||
return LicenseStatus(False, "HWID incompativel", hardware_id, 0)
|
||||
|
||||
expires_at = datetime.strptime(payload["e"], "%Y-%m-%d")
|
||||
days_remaining = (expires_at.date() - datetime.now().date()).days
|
||||
if days_remaining < 0:
|
||||
return LicenseStatus(False, "Licenca expirada", hardware_id, 0)
|
||||
|
||||
return LicenseStatus(True, f"Licenciado para {payload['c']}", hardware_id, days_remaining)
|
||||
except (InvalidToken, KeyError, ValueError, OSError, json.JSONDecodeError) as exc:
|
||||
LOGGER.warning("Falha ao verificar licenca: %s", exc)
|
||||
return LicenseStatus(False, "Serial invalido", hardware_id, 0)
|
||||
|
||||
def save(self, serial: str) -> None:
|
||||
cleaned = serial.strip()
|
||||
if not cleaned:
|
||||
raise ValueError("Serial vazio.")
|
||||
self.license_path.write_text(cleaned, encoding="utf-8")
|
||||
|
||||
|
||||
class RuntimeState:
|
||||
def __init__(self, base_dir: Path, default_password: str):
|
||||
self.lock = threading.RLock()
|
||||
self.config_store = JsonStore(base_dir / "config_nodes.json")
|
||||
self.map_store = JsonStore(base_dir / "mapa_dispositivos.json")
|
||||
self.config = normalize_config(self.config_store.load(build_default_config(default_password)), default_password)
|
||||
self.mapa_dispositivos = self.map_store.load({})
|
||||
self.dados_tempo_real: Dict[str, dict] = {}
|
||||
self.status_nodes_web: Dict[str, str] = {}
|
||||
self.restart_event = threading.Event()
|
||||
self.stop_event = threading.Event()
|
||||
self.save_config()
|
||||
|
||||
def snapshot(self) -> Tuple[dict, dict, dict, dict]:
|
||||
with self.lock:
|
||||
return (
|
||||
dict(self.config),
|
||||
dict(self.mapa_dispositivos),
|
||||
dict(self.dados_tempo_real),
|
||||
dict(self.status_nodes_web),
|
||||
)
|
||||
|
||||
def save_config(self) -> None:
|
||||
with self.lock:
|
||||
self.config_store.save(self.config)
|
||||
|
||||
def trigger_restart(self) -> None:
|
||||
self.restart_event.set()
|
||||
|
||||
def consume_restart(self) -> bool:
|
||||
if self.restart_event.is_set():
|
||||
self.restart_event.clear()
|
||||
return True
|
||||
return False
|
||||
|
||||
def authenticate(self, username: str, password: str) -> bool:
|
||||
with self.lock:
|
||||
if username != self.config["web_user"]:
|
||||
return False
|
||||
return check_password_hash(self.config["web_password_hash"], password)
|
||||
|
||||
def update_config(self, zabbix_server: str, hostname_zabbix: str, nodes: List[dict]) -> None:
|
||||
with self.lock:
|
||||
self.config["zabbix_server"] = zabbix_server.strip()
|
||||
self.config["hostname_zabbix"] = hostname_zabbix.strip()
|
||||
self.config["nodes"] = nodes
|
||||
|
||||
active_prefixes = [f"{node['ip']}_{node['unit']}_" for node in nodes]
|
||||
self.mapa_dispositivos = {
|
||||
key: value
|
||||
for key, value in self.mapa_dispositivos.items()
|
||||
if any(key.startswith(prefix) for prefix in active_prefixes)
|
||||
}
|
||||
|
||||
self.config_store.save(self.config)
|
||||
self.map_store.save(self.mapa_dispositivos)
|
||||
self.restart_event.set()
|
||||
|
||||
def rename_device(self, address_key: str, label: str) -> bool:
|
||||
with self.lock:
|
||||
if address_key not in self.mapa_dispositivos:
|
||||
return False
|
||||
self.mapa_dispositivos[address_key]["label"] = label.strip()
|
||||
self.map_store.save(self.mapa_dispositivos)
|
||||
self.restart_event.set()
|
||||
return True
|
||||
|
||||
def replace_node_devices(self, node: dict, discovered_map: dict) -> None:
|
||||
prefix = f"{node['ip']}_{node['unit']}_"
|
||||
with self.lock:
|
||||
self.mapa_dispositivos = {
|
||||
key: value for key, value in self.mapa_dispositivos.items() if not key.startswith(prefix)
|
||||
}
|
||||
self.mapa_dispositivos.update(discovered_map)
|
||||
self.map_store.save(self.mapa_dispositivos)
|
||||
self.restart_event.set()
|
||||
|
||||
|
||||
class MonitorEngine:
|
||||
def __init__(self, runtime_state: RuntimeState, license_service: LicenseService):
|
||||
self.runtime_state = runtime_state
|
||||
self.license_service = license_service
|
||||
|
||||
def start(self) -> None:
|
||||
threading.Thread(target=self.run, daemon=True, name="monitor-engine").start()
|
||||
|
||||
def run(self) -> None:
|
||||
while not self.runtime_state.stop_event.is_set():
|
||||
license_status = self.license_service.verify()
|
||||
if not license_status.valid:
|
||||
with self.runtime_state.lock:
|
||||
self.runtime_state.status_nodes_web = {"SISTEMA": "BLOQUEADO"}
|
||||
self.runtime_state.dados_tempo_real = {}
|
||||
time.sleep(RETRY_INTERVAL_SECONDS)
|
||||
continue
|
||||
|
||||
self.runtime_state.consume_restart()
|
||||
config, device_map, _, _ = self.runtime_state.snapshot()
|
||||
hostname = config.get("hostname_zabbix", "").strip()
|
||||
sender = self._build_sender(config.get("zabbix_server", "").strip())
|
||||
|
||||
self._send_discovery(sender, hostname, config.get("nodes", []), device_map)
|
||||
|
||||
while not self.runtime_state.stop_event.is_set():
|
||||
if self.runtime_state.consume_restart():
|
||||
break
|
||||
self._poll_once(sender, hostname)
|
||||
time.sleep(POLL_INTERVAL_SECONDS)
|
||||
|
||||
def _build_sender(self, server: str):
|
||||
if not server:
|
||||
return None
|
||||
try:
|
||||
return Sender(server=server)
|
||||
except Exception as exc:
|
||||
LOGGER.warning("Nao foi possivel iniciar sender do Zabbix: %s", exc)
|
||||
return None
|
||||
|
||||
def _send_discovery(self, sender, hostname: str, nodes: List[dict], device_map: dict) -> None:
|
||||
if not sender or not hostname or not device_map:
|
||||
return
|
||||
|
||||
lld = {"data": []}
|
||||
for info in device_map.values():
|
||||
lld["data"].append({"{#PONTO_LABEL}": info["label"]})
|
||||
for node in nodes:
|
||||
lld["data"].append({"{#NODE_NOME}": node["nome"]})
|
||||
|
||||
try:
|
||||
sender.send([ItemValue(hostname, "notifier.discovery", json.dumps(lld, ensure_ascii=False))])
|
||||
except Exception as exc:
|
||||
LOGGER.warning("Falha ao enviar discovery ao Zabbix: %s", exc)
|
||||
|
||||
def _poll_once(self, sender, hostname: str) -> None:
|
||||
config, device_map, _, _ = self.runtime_state.snapshot()
|
||||
nodes = config.get("nodes", [])
|
||||
metrics: List[ItemValue] = []
|
||||
latest_data: Dict[str, dict] = {}
|
||||
node_statuses: Dict[str, str] = {}
|
||||
|
||||
for node in nodes:
|
||||
node_metrics, node_data, node_status = self._poll_node(node, device_map, hostname)
|
||||
metrics.extend(node_metrics)
|
||||
latest_data.update(node_data)
|
||||
node_statuses[node["nome"]] = node_status
|
||||
|
||||
with self.runtime_state.lock:
|
||||
self.runtime_state.dados_tempo_real.update(latest_data)
|
||||
self.runtime_state.status_nodes_web = node_statuses
|
||||
|
||||
if sender and hostname and metrics:
|
||||
try:
|
||||
sender.send(metrics)
|
||||
except Exception as exc:
|
||||
LOGGER.warning("Falha ao enviar metricas ao Zabbix: %s", exc)
|
||||
|
||||
def _poll_node(self, node: dict, device_map: dict, hostname: str):
|
||||
client = ModbusClient(
|
||||
host=node["ip"],
|
||||
port=502,
|
||||
unit_id=node["unit"],
|
||||
auto_open=True,
|
||||
timeout=1,
|
||||
)
|
||||
node_metrics: List[ItemValue] = []
|
||||
node_data: Dict[str, dict] = {}
|
||||
node_status = "Offline"
|
||||
|
||||
try:
|
||||
alive = client.read_holding_registers(1, 1)
|
||||
status_conn = 1 if alive is not None else 0
|
||||
node_status = "Online" if status_conn else "Offline"
|
||||
|
||||
if hostname:
|
||||
node_metrics.append(ItemValue(hostname, f"node.status[{node['nome']}]", status_conn))
|
||||
|
||||
if not status_conn:
|
||||
return node_metrics, node_data, node_status
|
||||
|
||||
node_keys = [
|
||||
key for key in device_map.keys() if key.startswith(f"{node['ip']}_{node['unit']}_")
|
||||
]
|
||||
node_addresses = [int(key.rsplit("_", 1)[-1]) for key in node_keys]
|
||||
|
||||
for start, end in contiguous_ranges(node_addresses):
|
||||
registers = client.read_holding_registers(start, end - start + 1)
|
||||
if not registers:
|
||||
continue
|
||||
for offset, value in enumerate(registers):
|
||||
address = start + offset
|
||||
key = f"{node['ip']}_{node['unit']}_{address}"
|
||||
if key not in device_map:
|
||||
continue
|
||||
label = device_map[key]["label"]
|
||||
node_data[key] = {
|
||||
"label": label,
|
||||
"status": value,
|
||||
"node": node["nome"],
|
||||
"last": time.strftime("%H:%M:%S"),
|
||||
}
|
||||
if hostname:
|
||||
node_metrics.append(ItemValue(hostname, f"notifier.status[{label}]", value & 0xFF))
|
||||
node_metrics.append(ItemValue(hostname, f"notifier.comm[{label}]", (value >> 8) & 0xFF))
|
||||
except Exception as exc:
|
||||
LOGGER.warning("Falha ao consultar node %s (%s): %s", node["nome"], node["ip"], exc)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return node_metrics, node_data, node_status
|
||||
@@ -0,0 +1,150 @@
|
||||
from flask import current_app, flash, jsonify, redirect, render_template, request, url_for
|
||||
from flask_login import current_user, login_required, login_user, logout_user
|
||||
|
||||
from .core import LicenseService, RuntimeState, User, discover_node, get_status_info, validate_node_payload
|
||||
|
||||
|
||||
def get_runtime_state() -> RuntimeState:
|
||||
return current_app.extensions["runtime_state"]
|
||||
|
||||
|
||||
def get_license_service() -> LicenseService:
|
||||
return current_app.extensions["license_service"]
|
||||
|
||||
|
||||
def json_error(message: str, status_code: int = 400):
|
||||
return jsonify({"status": "error", "message": message}), status_code
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
if get_runtime_state().authenticate(username, password):
|
||||
login_user(User(username))
|
||||
return redirect(url_for("index"))
|
||||
flash("Usuario ou senha invalidos.")
|
||||
return render_template("login.html")
|
||||
|
||||
@app.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
config, _, _, _ = get_runtime_state().snapshot()
|
||||
license_status = get_license_service().verify()
|
||||
return render_template(
|
||||
"index.html",
|
||||
config=config,
|
||||
licenciado=license_status.valid,
|
||||
lic_msg=license_status.message,
|
||||
hwid=license_status.hardware_id,
|
||||
dias_restantes=license_status.days_remaining,
|
||||
)
|
||||
|
||||
@app.route("/api/nodes", methods=["POST"])
|
||||
@login_required
|
||||
def api_nodes():
|
||||
payload = request.get_json(silent=True)
|
||||
if not isinstance(payload, dict):
|
||||
return json_error("JSON invalido.")
|
||||
|
||||
try:
|
||||
zabbix_server = str(payload.get("zabbix_server", "")).strip()
|
||||
hostname_zabbix = str(payload.get("hostname_zabbix", "")).strip()
|
||||
nodes = [validate_node_payload(node) for node in payload.get("nodes", [])]
|
||||
except ValueError as exc:
|
||||
return json_error(str(exc))
|
||||
|
||||
if not zabbix_server:
|
||||
return json_error("Zabbix Server e obrigatorio.")
|
||||
if not hostname_zabbix:
|
||||
return json_error("Hostname Zabbix e obrigatorio.")
|
||||
|
||||
get_runtime_state().update_config(zabbix_server, hostname_zabbix, nodes)
|
||||
return jsonify({"status": "success"})
|
||||
|
||||
@app.route("/api/rename", methods=["POST"])
|
||||
@login_required
|
||||
def api_rename():
|
||||
payload = request.get_json(silent=True)
|
||||
if not isinstance(payload, dict):
|
||||
return json_error("JSON invalido.")
|
||||
|
||||
address_key = str(payload.get("addr", "")).strip()
|
||||
label = str(payload.get("label", "")).strip()
|
||||
|
||||
if not address_key:
|
||||
return json_error("Endereco do dispositivo e obrigatorio.")
|
||||
if not label:
|
||||
return json_error("Nome do dispositivo e obrigatorio.")
|
||||
|
||||
if not get_runtime_state().rename_device(address_key, label):
|
||||
return json_error("Dispositivo nao encontrado.", 404)
|
||||
|
||||
return jsonify({"status": "success"})
|
||||
|
||||
@app.route("/api/license", methods=["POST"])
|
||||
def api_license():
|
||||
payload = request.get_json(silent=True)
|
||||
if not isinstance(payload, dict):
|
||||
return json_error("JSON invalido.")
|
||||
|
||||
try:
|
||||
get_license_service().save(str(payload.get("serial", "")))
|
||||
except ValueError as exc:
|
||||
return json_error(str(exc))
|
||||
|
||||
get_runtime_state().trigger_restart()
|
||||
return jsonify({"status": "success"})
|
||||
|
||||
@app.route("/api/rescan_node", methods=["POST"])
|
||||
@login_required
|
||||
def api_rescan():
|
||||
payload = request.get_json(silent=True)
|
||||
if not isinstance(payload, dict):
|
||||
return json_error("JSON invalido.")
|
||||
|
||||
try:
|
||||
node = validate_node_payload(payload)
|
||||
except ValueError as exc:
|
||||
return json_error(str(exc))
|
||||
|
||||
_, current_map, _, _ = get_runtime_state().snapshot()
|
||||
discovered_map = discover_node(node, current_map)
|
||||
get_runtime_state().replace_node_devices(node, discovered_map)
|
||||
return jsonify({"status": "success", "count": len(discovered_map)})
|
||||
|
||||
@app.route("/api/data")
|
||||
@login_required
|
||||
def api_data():
|
||||
_, device_map, realtime_data, node_statuses = get_runtime_state().snapshot()
|
||||
sensors = []
|
||||
|
||||
for key, info in device_map.items():
|
||||
data = realtime_data.get(
|
||||
key,
|
||||
{"status": 0, "last": "--:--:--", "node": info.get("node_nome", "...")},
|
||||
)
|
||||
status_info = get_status_info(int(data["status"]))
|
||||
sensors.append(
|
||||
{
|
||||
"key": key,
|
||||
"label": info["label"],
|
||||
"node": data.get("node", "..."),
|
||||
"status_text": status_info["label"],
|
||||
"color": status_info["color"],
|
||||
"last": data["last"],
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({"sensores": sensors, "nodes_status": node_statuses})
|
||||
|
||||
@app.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("login"))
|
||||
Reference in New Issue
Block a user