| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- <?php declare(strict_types = 0);
- /*
- ** L2 Topology - persistence & configuration layer.
- **
- ** Two tables are auto-created (portable: works on any Zabbix DB the web user
- ** can write to; the compose init sidecar also creates them as root):
- **
- ** l2_topology_history : one compressed snapshot per collection, kept 7 days
- ** l2_topology_config : single row (id=1) holding every display/collection knob
- **
- ** Snapshots are gzip-compressed MEDIUMBLOBs so a 5-minute cadence for a week
- ** stays around ~100-150 MB instead of >1 GB of raw JSON.
- */
- namespace Modules\WecomCenter\Lib;
- use DB;
- use Throwable;
- class L2Store {
- public const HISTORY_TABLE = 'l2_topology_history';
- public const CONFIG_TABLE = 'l2_topology_config';
- public const CONFIG_ID = 1;
- /** Full default configuration. Every key can be overridden from the UI. */
- public static function defaults(): array {
- return [
- // Display page ------------------------------------------------------
- 'refresh_seconds' => 300, // auto-refresh of the live view
- 'root_label' => 'COBCO', // outermost frame label
- // Physical networks. A switch belongs to the first network whose
- // keyword appears in its name or IP; adjacency is used as fallback.
- // key | label | keyword1,keyword2 | colour
- 'networks' => [
- ['key' => 'SURVE', 'label' => 'SURVE 监控网', 'keywords' => ['SURVE', '10.169.'], 'color' => '#2E75B6'],
- ['key' => 'OFFICE', 'label' => 'OFFICE 办公网', 'keywords' => ['OFFICE', '10.160.'], 'color' => '#548235']
- ],
- // Root/core switches. A network may have more than one core; every
- // match is drawn in the left-most core column. match = contains|regex.
- 'core_rules' => [
- ['net' => 'SURVE', 'match' => 'contains', 'pattern' => 'core surve'],
- ['net' => 'OFFICE', 'match' => 'contains', 'pattern' => 'core office']
- ],
- // Collection --------------------------------------------------------
- 'templates' => ['Huawei VRP by SNMP (walk)'],
- 'full_interval_min' => 5, // full topology/port collection
- 'alarm_interval_min'=> 1, // alarm-only refresh
- 'history_days' => 7,
- // Link colours (bits per second bucket) -----------------------------
- 'colors' => [
- '100' => '#F39C12', // 100M 橙色
- '1000' => '#2ECC71', // 1G 绿色
- '10000' => '#3498DB', // 10G 蓝色
- '20000' => '#9B59B6', // 20G+ 紫色
- 'unknown' => '#E74C3C', // 未知 红色
- 'down' => '#C0392B' // 中断 红色
- ],
- // Latency thresholds (ms) applied to switch nodes (host->switch ping)
- 'latency_warn' => 3.0,
- 'latency_bad' => 5.0,
- // Grouping rules: first matching rule wins, else "Other".
- // match = contains | regex ; label may contain $1..$9 backrefs.
- 'group_rules' => [
- ['match' => 'contains', 'pattern' => 'DataCenter', 'label' => 'DataCenter', 'big' => true],
- ['match' => 'contains', 'pattern' => 'Core', 'label' => 'Core', 'big' => true],
- ['match' => 'regex', 'pattern' => 'COBCO-(\\d{4}[A-Za-z]?)', 'label' => '$1', 'big' => false],
- ['match' => 'regex', 'pattern' => '^(\\d{4}[A-Za-z]?)', 'label' => '$1', 'big' => false]
- ],
- 'other_label' => 'Other'
- ];
- }
- /** Create tables if absent. Safe to call on every request (idempotent). */
- public static function ensureSchema(): void {
- try {
- DB::execute(
- 'CREATE TABLE IF NOT EXISTS '.self::HISTORY_TABLE.' ('.
- ' id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,'.
- ' ts BIGINT UNSIGNED NOT NULL,'.
- ' captured_at DATETIME NOT NULL,'.
- ' stats TEXT NOT NULL,'.
- ' snapshot MEDIUMBLOB NOT NULL,'.
- ' PRIMARY KEY (id),'.
- ' KEY idx_l2_ts (ts)'.
- ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin'
- );
- DB::execute(
- 'CREATE TABLE IF NOT EXISTS '.self::CONFIG_TABLE.' ('.
- ' id TINYINT UNSIGNED NOT NULL,'.
- ' config LONGTEXT NOT NULL,'.
- ' updated DATETIME NOT NULL,'.
- ' PRIMARY KEY (id)'.
- ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin'
- );
- }
- catch (Throwable $e) {
- // Table may already exist or the DB user may lack DDL rights; the
- // compose init sidecar / install doc handles that case. Never fail a page.
- }
- }
- public static function getConfig(): array {
- self::ensureSchema();
- $cfg = self::defaults();
- try {
- $rows = DB::select('SELECT config FROM '.self::CONFIG_TABLE.' WHERE id='.self::CONFIG_ID);
- if ($rows && isset($rows[0]['config'])) {
- $saved = json_decode((string) $rows[0]['config'], true);
- if (is_array($saved)) {
- $cfg = self::mergeDeep($cfg, $saved);
- }
- }
- }
- catch (Throwable $e) {
- // fall back to defaults
- }
- // Make sure the out-of-process collectors can always read the config
- // (write it once if the mirror file is still missing).
- $mirror = dirname(__DIR__).'/assets/l2topology/config.json';
- if (!is_file($mirror)) {
- self::writeMirror($cfg);
- }
- return $cfg;
- }
- public static function saveConfig(array $cfg): void {
- self::ensureSchema();
- $cfg = self::mergeDeep(self::defaults(), $cfg);
- $json = json_encode($cfg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
- try {
- DB::execute(
- 'INSERT INTO '.self::CONFIG_TABLE.' (id, config, updated) VALUES ('.self::CONFIG_ID.', ?, UTC_TIMESTAMP())'.
- ' ON DUPLICATE KEY UPDATE config=VALUES(config), updated=VALUES(updated)',
- [$json]
- );
- }
- catch (Throwable $e) {
- }
- self::writeMirror($cfg);
- }
- /** Mirror config to a JSON file so the out-of-process collectors can read it. */
- public static function writeMirror(array $cfg): void {
- $file = dirname(__DIR__).'/assets/l2topology/config.json';
- try {
- $tmp = $file.'.tmp';
- file_put_contents($tmp, json_encode($cfg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
- @chmod($tmp, 0644);
- @rename($tmp, $file);
- }
- catch (Throwable $e) {
- }
- }
- /** Newest-first list of stored snapshots (for the time selector). */
- public static function listHistory(int $limit = 500): array {
- $out = [];
- try {
- $rows = DB::select(
- 'SELECT ts, captured_at, stats FROM '.self::HISTORY_TABLE.' ORDER BY ts DESC LIMIT '.max(1, $limit)
- );
- foreach ($rows as $r) {
- $out[] = [
- 'ts' => (int) $r['ts'],
- 'captured_at' => (string) $r['captured_at'],
- 'stats' => json_decode((string) $r['stats'], true)
- ];
- }
- }
- catch (Throwable $e) {
- }
- return $out;
- }
- /**
- * Snapshot JSON at (or just before) $ts. When $ts is null the newest stored
- * snapshot is used, falling back to the live snapshot.json file.
- */
- public static function snapshotAt(?int $ts): array {
- $json = null;
- $used = $ts;
- try {
- if ($ts === null) {
- $rows = DB::select('SELECT ts, snapshot FROM '.self::HISTORY_TABLE.' ORDER BY ts DESC LIMIT 1');
- }
- else {
- $rows = DB::select(
- 'SELECT ts, snapshot FROM '.self::HISTORY_TABLE.' WHERE ts<=? ORDER BY ts DESC LIMIT 1',
- [$ts]
- );
- if (!$rows) {
- $rows = DB::select('SELECT ts, snapshot FROM '.self::HISTORY_TABLE.' ORDER BY ts ASC LIMIT 1');
- }
- }
- if ($rows) {
- $blob = $rows[0]['snapshot'];
- $raw = @gzdecode($blob);
- if ($raw !== false && $raw !== '') {
- $json = $raw;
- $used = (int) $rows[0]['ts'];
- }
- }
- }
- catch (Throwable $e) {
- }
- if ($json === null) {
- $file = dirname(__DIR__).'/assets/l2topology/snapshot.json';
- if (is_readable($file)) {
- $raw = file_get_contents($file);
- if ($raw !== false && $raw !== '') {
- $json = $raw;
- }
- }
- }
- return ['json' => $json, 'ts' => $used];
- }
- private static function mergeDeep(array $base, array $over): array {
- foreach ($over as $k => $v) {
- if (is_array($v) && isset($base[$k]) && is_array($base[$k]) && self::isAssoc($base[$k])) {
- $base[$k] = self::mergeDeep($base[$k], $v);
- }
- else {
- $base[$k] = $v;
- }
- }
- return $base;
- }
- private static function isAssoc(array $a): bool {
- return array_keys($a) !== range(0, count($a) - 1);
- }
- }
|