|
|
@@ -0,0 +1,344 @@
|
|
|
+<?php declare(strict_types = 0);
|
|
|
+/*
|
|
|
+** WeCom Center - business logic helpers.
|
|
|
+** Works through the official Zabbix API so every setting is stored in the
|
|
|
+** Zabbix database itself -> survives image upgrades and is portable.
|
|
|
+*/
|
|
|
+
|
|
|
+namespace Modules\WecomCenter\Lib;
|
|
|
+
|
|
|
+use API;
|
|
|
+use Exception;
|
|
|
+
|
|
|
+class Helper {
|
|
|
+
|
|
|
+ // Zabbix trigger severity index => media severity bit (1 << index).
|
|
|
+ public const SEVERITY_BITS = [1, 2, 4, 8, 16, 32];
|
|
|
+
|
|
|
+ // Built-in alert schemes: name => [severity_mask, active(0=enabled,1=disabled)]
|
|
|
+ public const SCHEMES = [
|
|
|
+ 'standard' => [48, 0], // High + Disaster (recommended)
|
|
|
+ 'warnplus' => [56, 0], // Average + High + Disaster
|
|
|
+ 'all' => [63, 0], // everything
|
|
|
+ 'quiet' => [32, 0], // Disaster only
|
|
|
+ 'off' => [63, 1] // alerting disabled
|
|
|
+ ];
|
|
|
+
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+ /* Media types / robot */
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * All webhook media types (type == 4).
|
|
|
+ */
|
|
|
+ public static function webhookMediatypes(): array {
|
|
|
+ try {
|
|
|
+ $res = API::Mediatype()->get([
|
|
|
+ 'output' => ['mediatypeid', 'name', 'type', 'status'],
|
|
|
+ 'selectParameters' => 'extend',
|
|
|
+ 'filter' => ['type' => 4]
|
|
|
+ ]);
|
|
|
+ return is_array($res) ? $res : [];
|
|
|
+ }
|
|
|
+ catch (\Throwable $e) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function getMediatype(string $mediatypeid): ?array {
|
|
|
+ try {
|
|
|
+ $res = API::Mediatype()->get([
|
|
|
+ 'output' => ['mediatypeid', 'name', 'type', 'status'],
|
|
|
+ 'selectParameters' => 'extend',
|
|
|
+ 'mediatypeids' => [$mediatypeid]
|
|
|
+ ]);
|
|
|
+ return (is_array($res) && $res) ? $res[0] : null;
|
|
|
+ }
|
|
|
+ catch (\Throwable $e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Read the webhook URL / key from a media type parameters list.
|
|
|
+ */
|
|
|
+ public static function webhookUrl(array $mediatype): string {
|
|
|
+ foreach (($mediatype['parameters'] ?? []) as $p) {
|
|
|
+ $name = strtolower($p['name']);
|
|
|
+ if (in_array($name, ['webhook_url', 'url', 'webhook'], true)) {
|
|
|
+ return (string) $p['value'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Update (or add) the webhook_url parameter of a media type, preserving others.
|
|
|
+ */
|
|
|
+ public static function setWebhookUrl(string $mediatypeid, string $url): void {
|
|
|
+ $mt = self::getMediatype($mediatypeid);
|
|
|
+ if (!$mt) {
|
|
|
+ throw new Exception('Media type not found: '.$mediatypeid);
|
|
|
+ }
|
|
|
+ $params = [];
|
|
|
+ $found = false;
|
|
|
+ foreach (($mt['parameters'] ?? []) as $p) {
|
|
|
+ if (in_array(strtolower($p['name']), ['webhook_url', 'url', 'webhook'], true)) {
|
|
|
+ $params[] = ['name' => $p['name'], 'value' => $url];
|
|
|
+ $found = true;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ $params[] = ['name' => $p['name'], 'value' => $p['value']];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!$found) {
|
|
|
+ $params[] = ['name' => 'webhook_url', 'value' => $url];
|
|
|
+ }
|
|
|
+ API::Mediatype()->update(['mediatypeid' => $mediatypeid, 'parameters' => $params]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+ /* Robot user + media (severity / active) */
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Find the dedicated robot user with its media.
|
|
|
+ */
|
|
|
+ public static function robotUser(string $username = 'wecomrobot'): ?array {
|
|
|
+ $res = API::User()->get([
|
|
|
+ 'output' => ['userid', 'username'],
|
|
|
+ 'selectMedias' => 'extend',
|
|
|
+ 'filter' => ['username' => $username]
|
|
|
+ ]);
|
|
|
+ return $res ? $res[0] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Current severity mask + active flag of a user's media for a media type.
|
|
|
+ * Returns ['active'=>int, 'severity'=>int] or null when media absent.
|
|
|
+ */
|
|
|
+ public static function currentMedia(?array $user, string $mediatypeid): ?array {
|
|
|
+ if (!$user || empty($user['medias'])) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ foreach ($user['medias'] as $m) {
|
|
|
+ if ((string) $m['mediatypeid'] === (string) $mediatypeid) {
|
|
|
+ return [
|
|
|
+ 'active' => (int) $m['active'],
|
|
|
+ 'severity' => (int) $m['severity'],
|
|
|
+ 'period' => $m['period'] ?? '1-7,00:00-24:00',
|
|
|
+ 'sendto' => $m['sendto'] ?? ''
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Ensure a robot user exists; create one (no frontend access) if missing.
|
|
|
+ */
|
|
|
+ public static function ensureRobotUser(string $username): array {
|
|
|
+ $user = self::robotUser($username);
|
|
|
+ if ($user) {
|
|
|
+ return $user;
|
|
|
+ }
|
|
|
+
|
|
|
+ // best-effort group / role for a non-login notification account
|
|
|
+ $grp = API::UserGroup()->get([
|
|
|
+ 'output' => ['usrgrpid', 'name'],
|
|
|
+ 'filter' => ['name' => 'No access to the frontend'],
|
|
|
+ 'limit' => 1
|
|
|
+ ]);
|
|
|
+ $usrgrpid = $grp ? $grp[0]['usrgrpid'] : null;
|
|
|
+ if ($usrgrpid === null) {
|
|
|
+ $all = API::UserGroup()->get(['output' => ['usrgrpid'], 'limit' => 1]);
|
|
|
+ $usrgrpid = $all ? $all[0]['usrgrpid'] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $role = API::Role()->get([
|
|
|
+ 'output' => ['roleid', 'name'],
|
|
|
+ 'filter' => ['name' => 'User role'],
|
|
|
+ 'limit' => 1
|
|
|
+ ]);
|
|
|
+ $roleid = $role ? $role[0]['roleid'] : null;
|
|
|
+
|
|
|
+ $create = [
|
|
|
+ 'username' => $username,
|
|
|
+ 'passwd' => bin2hex(random_bytes(16)).'Aa1!',
|
|
|
+ 'usrgrps' => [['usrgrpid' => $usrgrpid]]
|
|
|
+ ];
|
|
|
+ if ($roleid !== null) {
|
|
|
+ $create['roleid'] = $roleid;
|
|
|
+ }
|
|
|
+
|
|
|
+ $res = API::User()->create($create);
|
|
|
+ return self::robotUser($username);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Apply severity mask + active flag to a user's media for a media type.
|
|
|
+ * Rebuilds the whole media list (API user.update replaces medias).
|
|
|
+ */
|
|
|
+ public static function applyMedia(string $userid, string $mediatypeid, int $severity, int $active,
|
|
|
+ string $sendto = 'WeCom') {
|
|
|
+ $res = API::User()->get([
|
|
|
+ 'output' => ['userid'],
|
|
|
+ 'selectMedias' => 'extend',
|
|
|
+ 'userids' => [$userid]
|
|
|
+ ]);
|
|
|
+ $medias = [];
|
|
|
+ if ($res) {
|
|
|
+ foreach (($res[0]['medias'] ?? []) as $m) {
|
|
|
+ $medias[] = [
|
|
|
+ 'mediatypeid' => $m['mediatypeid'],
|
|
|
+ 'sendto' => $m['sendto'],
|
|
|
+ 'severity' => (string) $m['severity'],
|
|
|
+ 'active' => (string) $m['active'],
|
|
|
+ 'period' => $m['period'] ?: '1-7,00:00-24:00'
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $hit = false;
|
|
|
+ foreach ($medias as &$m) {
|
|
|
+ if ((string) $m['mediatypeid'] === (string) $mediatypeid) {
|
|
|
+ $m['severity'] = (string) $severity;
|
|
|
+ $m['active'] = (string) $active;
|
|
|
+ if ($sendto !== '') {
|
|
|
+ $m['sendto'] = $sendto;
|
|
|
+ }
|
|
|
+ $hit = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ unset($m);
|
|
|
+ if (!$hit) {
|
|
|
+ $medias[] = [
|
|
|
+ 'mediatypeid' => $mediatypeid,
|
|
|
+ 'sendto' => $sendto !== '' ? $sendto : 'WeCom',
|
|
|
+ 'severity' => (string) $severity,
|
|
|
+ 'active' => (string) $active,
|
|
|
+ 'period' => '1-7,00:00-24:00'
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ API::User()->update(['userid' => $userid, 'medias' => $medias]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+ /* Send test message */
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+
|
|
|
+ public static function testWebhook(string $url, string $text): array {
|
|
|
+ $payload = json_encode(['msgtype' => 'text', 'text' => ['content' => $text]], JSON_UNESCAPED_UNICODE);
|
|
|
+ $ch = curl_init($url);
|
|
|
+ curl_setopt_array($ch, [
|
|
|
+ CURLOPT_RETURNTRANSFER => true,
|
|
|
+ CURLOPT_POST => true,
|
|
|
+ CURLOPT_POSTFIELDS => $payload,
|
|
|
+ CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
|
+ CURLOPT_TIMEOUT => 15,
|
|
|
+ CURLOPT_SSL_VERIFYPEER => false,
|
|
|
+ CURLOPT_SSL_VERIFYHOST => 0
|
|
|
+ ]);
|
|
|
+ $body = curl_exec($ch);
|
|
|
+ $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
+ $err = curl_error($ch);
|
|
|
+ curl_close($ch);
|
|
|
+ return ['status' => $status, 'body' => $body !== false ? $body : $err];
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+ /* Templates */
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Bundled templates shipped inside modules/WecomCenter/templates/.
|
|
|
+ */
|
|
|
+ public static function bundledTemplates(string $module_dir): array {
|
|
|
+ $dir = rtrim($module_dir, '/').'/templates';
|
|
|
+ $out = [];
|
|
|
+ if (!is_dir($dir)) {
|
|
|
+ return $out;
|
|
|
+ }
|
|
|
+ foreach (scandir($dir) as $f) {
|
|
|
+ if ($f === '.' || $f === '..') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
|
|
|
+ if (in_array($ext, ['xml', 'yaml', 'yml', 'json'], true)) {
|
|
|
+ $out[] = ['file' => $f, 'name' => pathinfo($f, PATHINFO_FILENAME)];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $out;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function importTemplate(string $module_dir, string $file): array {
|
|
|
+ $dir = rtrim($module_dir, '/').'/templates';
|
|
|
+ $file = basename($file);
|
|
|
+ $path = $dir.'/'.$file;
|
|
|
+ if (!is_file($path)) {
|
|
|
+ throw new Exception('Template file not found: '.$file);
|
|
|
+ }
|
|
|
+ $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
|
|
+ $format = ['xml' => 'xml', 'json' => 'json', 'yaml' => 'yaml', 'yml' => 'yaml'][$ext] ?? 'yaml';
|
|
|
+ $source = file_get_contents($path);
|
|
|
+ $result = API::Configuration()->import([
|
|
|
+ 'format' => $format,
|
|
|
+ 'source' => $source,
|
|
|
+ 'rules' => [
|
|
|
+ 'template_groups' => ['createMissing' => true, 'updateExisting' => true],
|
|
|
+ 'templates' => ['createMissing' => true, 'updateExisting' => true],
|
|
|
+ 'items' => ['createMissing' => true, 'updateExisting' => true],
|
|
|
+ 'triggers' => ['createMissing' => true, 'updateExisting' => true]
|
|
|
+ ]
|
|
|
+ ]);
|
|
|
+ return is_array($result) ? $result : [];
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+ /* Map coordinates */
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+
|
|
|
+ public static function hosts(): array {
|
|
|
+ return API::Host()->get([
|
|
|
+ 'output' => ['hostid', 'name'],
|
|
|
+ 'sortfield' => 'name',
|
|
|
+ 'limit' => 1000
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function setHostLocation(string $hostid, string $lat, string $lon): void {
|
|
|
+ API::Host()->update([
|
|
|
+ 'hostid' => $hostid,
|
|
|
+ 'inventory_mode' => 0, // manual
|
|
|
+ 'inventory' => [
|
|
|
+ 'location_lat' => $lat,
|
|
|
+ 'location_lon' => $lon
|
|
|
+ ]
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function hostLocation(string $hostid): array {
|
|
|
+ $res = API::Host()->get([
|
|
|
+ 'output' => ['hostid', 'name'],
|
|
|
+ 'selectInventory' => ['location_lat', 'location_lon'],
|
|
|
+ 'hostids' => [$hostid]
|
|
|
+ ]);
|
|
|
+ return $res ? $res[0] : [];
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+ /* Helpers */
|
|
|
+ /* ------------------------------------------------------------------ */
|
|
|
+
|
|
|
+ public static function severityMask(array $indices): int {
|
|
|
+ $mask = 0;
|
|
|
+ foreach ($indices as $i) {
|
|
|
+ $i = (int) $i;
|
|
|
+ if ($i >= 1 && $i <= 6) {
|
|
|
+ $mask |= self::SEVERITY_BITS[$i - 1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $mask;
|
|
|
+ }
|
|
|
+}
|