|
|
@@ -832,4 +832,167 @@ class Helper {
|
|
|
}
|
|
|
return $n;
|
|
|
}
|
|
|
+
|
|
|
+ /* ================================================================== */
|
|
|
+ /* ONE-CLICK ENVIRONMENT INITIALISATION (fresh Zabbix bootstrap) */
|
|
|
+ /* Creates whatever is missing so the module works out of the box: */
|
|
|
+ /* {$WECOM_KEY} macro, webhook media type, robot user + media, */
|
|
|
+ /* and the trigger action. Existing objects are reused, never duped. */
|
|
|
+ /* ================================================================== */
|
|
|
+
|
|
|
+ public const MEDIATYPE_NAME = 'WeCom Group Robot';
|
|
|
+ public const ROBOT_USERNAME = 'wecomrobot';
|
|
|
+ public const ACTION_NAME = 'Send alarm by Wecom Robot';
|
|
|
+ public const ZBX_URL_MACRO = '{$ZBX_URL}';
|
|
|
+
|
|
|
+ public const WEBHOOK_SCRIPT = <<<'JS'
|
|
|
+var params = JSON.parse(value);
|
|
|
+var content = params.message || 'Zabbix';
|
|
|
+var url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=' + params.key;
|
|
|
+function send(payload){
|
|
|
+ var r = new HttpRequest();
|
|
|
+ r.addHeader('Content-Type: application/json');
|
|
|
+ var resp = r.post(url, JSON.stringify(payload));
|
|
|
+ return {status: r.getStatus(), body: resp};
|
|
|
+}
|
|
|
+var res = send({msgtype: 'markdown', markdown: {content: content}});
|
|
|
+if (res.status !== 200) {
|
|
|
+ var res2 = send({msgtype: 'text', text: {content: content}});
|
|
|
+ if (res2.status !== 200) {
|
|
|
+ Zabbix.Log(4, 'WeCom webhook error HTTP ' + res.status + ' body: ' + res.body);
|
|
|
+ throw 'HTTP ' + res.status + ' => ' + res.body;
|
|
|
+ }
|
|
|
+}
|
|
|
+return 'Message sent successfully';
|
|
|
+JS;
|
|
|
+
|
|
|
+ /** Find an existing WeCom webhook media type (ours by name, else one bound to {$WECOM_KEY}). */
|
|
|
+ public static function findWebhookMediatype(): ?array {
|
|
|
+ $mts = self::webhookMediatypes();
|
|
|
+ foreach ($mts as $m) {
|
|
|
+ if (isset($m['name']) && $m['name'] === self::MEDIATYPE_NAME) {
|
|
|
+ return $m;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ foreach ($mts as $m) {
|
|
|
+ foreach (($m['parameters'] ?? []) as $p) {
|
|
|
+ if (strtolower((string) $p['name']) === 'key' && strpos((string) $p['value'], 'WECOM_KEY') !== false) {
|
|
|
+ return $m;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ foreach ($mts as $m) {
|
|
|
+ if (stripos((string) $m['name'], 'wecom robot') !== false) {
|
|
|
+ return $m;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Ensure the WeCom webhook media type exists. @return array{0:string,1:bool} [mediatypeid, created] */
|
|
|
+ public static function ensureMediatype(): array {
|
|
|
+ $found = self::findWebhookMediatype();
|
|
|
+ if ($found) {
|
|
|
+ return [(string) $found['mediatypeid'], false];
|
|
|
+ }
|
|
|
+ $res = API::Mediatype()->create([
|
|
|
+ 'name' => self::MEDIATYPE_NAME,
|
|
|
+ 'type' => 4, // webhook
|
|
|
+ 'status' => 0, // enabled
|
|
|
+ 'message_format' => 1, // markdown
|
|
|
+ 'description' => 'Created by WeCom Center: posts alerts to a WeCom group robot.',
|
|
|
+ 'timeout' => '30s',
|
|
|
+ 'script' => self::WEBHOOK_SCRIPT,
|
|
|
+ 'parameters' => [
|
|
|
+ ['name' => 'message', 'value' => '{ALERT.MESSAGE}'],
|
|
|
+ ['name' => 'key', 'value' => self::KEY_MACRO]
|
|
|
+ ]
|
|
|
+ ]);
|
|
|
+ return [(string) $res['mediatypeids'][0], true];
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Ensure the trigger action exists and points to the robot user + media type. */
|
|
|
+ public static function ensureAction(string $mediatypeid, string $userid): bool {
|
|
|
+ $ex = API::Action()->get([
|
|
|
+ 'output' => ['actionid'],
|
|
|
+ 'filter' => ['name' => self::ACTION_NAME]
|
|
|
+ ]);
|
|
|
+ if ($ex) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $problem = "<font color=\"warning\">**🔴 {TRIGGER.SEVERITY} · {HOST.NAME}**</font>\n"
|
|
|
+ ."> 问题:{TRIGGER.NAME}\n"
|
|
|
+ ."> 时间:{EVENT.DATE} {EVENT.TIME} | 持续:{EVENT.AGE}\n"
|
|
|
+ ."> 当前值:{ITEM.LASTVALUE}\n"
|
|
|
+ ."[查看详情]({$ZBX_URL}/tr_events.php?triggerid={TRIGGER.ID}&eventid={EVENT.ID})";
|
|
|
+ $recovery = "<font color=\"info\">**🟢 已恢复 · {HOST.NAME}**</font>\n"
|
|
|
+ ."> 问题:{TRIGGER.NAME}\n"
|
|
|
+ ."> 恢复时间:{EVENT.RECOVERY.DATE} {EVENT.RECOVERY.TIME}\n"
|
|
|
+ ."> 故障持续:{EVENT.DURATION}\n"
|
|
|
+ ."[查看详情]({$ZBX_URL}/tr_events.php?triggerid={TRIGGER.ID}&eventid={EVENT.ID})";
|
|
|
+
|
|
|
+ API::Action()->create([
|
|
|
+ 'name' => self::ACTION_NAME,
|
|
|
+ 'eventsource' => 0, // trigger actions
|
|
|
+ 'status' => 0,
|
|
|
+ 'esc_period' => '10m',
|
|
|
+ 'filter' => ['evaltype' => 0, 'conditions' => []],
|
|
|
+ 'operations' => [[
|
|
|
+ 'operationtype' => 0,
|
|
|
+ 'esc_step_from' => 1,
|
|
|
+ 'esc_step_to' => 1,
|
|
|
+ 'opmessage' => [
|
|
|
+ 'default_msg' => 0,
|
|
|
+ 'subject' => '[Zabbix Alarm] {TRIGGER.NAME}',
|
|
|
+ 'message' => $problem,
|
|
|
+ 'mediatypeid' => $mediatypeid
|
|
|
+ ],
|
|
|
+ 'opmessage_usr' => [['userid' => $userid]]
|
|
|
+ ]],
|
|
|
+ 'recovery_operations' => [[
|
|
|
+ 'operationtype' => 0,
|
|
|
+ 'opmessage' => [
|
|
|
+ 'default_msg' => 0,
|
|
|
+ 'subject' => '[Zabbix Recovery] {TRIGGER.NAME}',
|
|
|
+ 'message' => $recovery,
|
|
|
+ 'mediatypeid' => $mediatypeid
|
|
|
+ ],
|
|
|
+ 'opmessage_usr' => [['userid' => $userid]]
|
|
|
+ ]]
|
|
|
+ ]);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Idempotent bootstrap. Safe to run repeatedly.
|
|
|
+ * @return array{key_set:bool,zbx_url:bool,mediatypeid:string,created_mediatype:bool,created_user:bool,created_action:bool}
|
|
|
+ */
|
|
|
+ public static function initializeEnvironment(string $key, string $zbxUrl = ''): array {
|
|
|
+ $out = [
|
|
|
+ 'key_set' => false, 'zbx_url' => false, 'mediatypeid' => '',
|
|
|
+ 'created_mediatype' => false, 'created_user' => false, 'created_action' => false
|
|
|
+ ];
|
|
|
+ if ($key !== '') {
|
|
|
+ self::setWebhookKey($key);
|
|
|
+ $out['key_set'] = true;
|
|
|
+ }
|
|
|
+ if ($zbxUrl !== '' && self::getGlobalMacro(self::ZBX_URL_MACRO) === null) {
|
|
|
+ self::setGlobalMacro(self::ZBX_URL_MACRO, $zbxUrl);
|
|
|
+ $out['zbx_url'] = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ [$mtid, $createdMt] = self::ensureMediatype();
|
|
|
+ $out['mediatypeid'] = $mtid;
|
|
|
+ $out['created_mediatype'] = $createdMt;
|
|
|
+
|
|
|
+ $existed = self::robotUser(self::ROBOT_USERNAME) !== null;
|
|
|
+ $user = self::ensureRobotUser(self::ROBOT_USERNAME);
|
|
|
+ $out['created_user'] = !$existed;
|
|
|
+ $uid = $user['userid'] ?? '';
|
|
|
+ if ($uid !== '') {
|
|
|
+ self::applyMedia((string) $uid, $mtid, 48, 0); // High + Disaster, enabled
|
|
|
+ $out['created_action'] = self::ensureAction($mtid, (string) $uid);
|
|
|
+ }
|
|
|
+ return $out;
|
|
|
+ }
|
|
|
}
|