その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

Zabbix 4.x で API を使って Active な Problem だけを引っ張ってくる方法

要旨

どうやれば Zabbix API を使って所望の機能を実現できるかを調査する際には Zabbix が実際に呼んでいる SQL クエリを tcpdump で抜き出すと手っ取り早い.

内容

色々あってZabbixを使っているわけですが,この手のものを使っているとAPIを介して Active なアラート (Zabbixでいうとproblem) を取りたくなります.
というわけで,Zabbix 4系でそれをやるにはどうしたら良いかということについて記すものです.

problem.get [Zabbix Documentation 4.4]

さてまずドキュメントを漁っていて使えそうに思えるのはこの辺でしょう.

This method is for retrieving unresolved problems.

などとそれっぽいことも書いています……がこのAPIだけ使っても駄目.なぜか Resolved な problem や,退役した host の problem も返脚されてしまいます *1


実際に欲しいものは Zabbix UI で取得できる unresolved な problems (つまり active な problems) なので,それと等価なクエリを発行してやれば良い……ということで Zabbix Server で tcpdump して, RDBMS に対してどのような SELECT クエリを発行しているかを見ます.
RDBMS (MySQL) に対するトラフィックについて tcpdump する方法については以下のような記事が詳しいでしょう:

memo.yuuk.io

実際にやってみると,とてつもない量のクエリが発行されます *2 が,めげずに読み解いてみると

  • problem table からフィルタ条件 (severityとか) で WHERE かけて引っ張ってくる
  • triggers table から相関サブクエリを用いて triggerid, itemid および hostid をどやこやして active な host, trigger, event を取ってくる

という感じで active な problems を抜き出してくる,という雰囲気になっています.tcpdump で実際に取れるクエリはこのような感じ:

SELECT
  p.eventid,p.objectid,p.clock,p.ns,p.name,p.severity FROM problem p
WHERE
  p.source='0' AND
  p.object='0' AND
  p.severity IN ('4','5') AND
  NOT EXISTS (
    SELECT NULL FROM event_suppress es
    WHERE es.eventid=p.eventid
  ) AND
  (p.r_eventid IS NULL OR p.r_clock>1579414413)
ORDER BY p.eventid
DESC LIMIT 1001

SELECT
  t.triggerid,t.priority FROM triggers t
WHERE
  t.triggerid IN (/* IDs here */) AND
  NOT EXISTS (
    SELECT NULL FROM functions f, items i, hosts h
    WHERE
      t.triggerid=f.triggerid AND
      f.itemid=i.itemid AND
      i.hostid=h.hostid AND
      (i.status<>0 OR h.status<>0)
  ) AND
  t.status=0 AND
  t.flags IN ('0','4')

これを API だけでやろうとすると

  • problem.get API を呼んで event ID のリストを引っ張ってくる
  • event ID のリストを使って event.get API を呼んで
    • trigger ID のリストを引っ張ってくる
    • trigger ID => event なマップを作る
  • trigger ID のリストを使って trigger.get API を呼んで
    • item ID のリストを引っ張ってくる
    • trigger ID => trigger なマップを作る
  • item ID のリストを使ってitem.get API を呼んで host ID のリストを引っ張ってくる
    • この時 item の status が enabled かどうかによってフィルタする
  • host ID のリストを使って host.get API を呼んで
    • host の status が enable かどうかによってフィルタする
    • host に紐付いている triggers を走査して
      • trigger ID => trigger なマップから trigger を特定する
      • trigger ID => event なマップから event を特定する

とやってやると Zabbix UI で取れる active な problems を 5発の API 呼び出しで抜き出すことが出来ます.めちゃめちゃ大変ですね!!!
おとなしくSQLを発行したほうが楽だったまであります.つらい……

まとめ

どうやれば Zabbix API を使って所望の機能を実現できるかを調査する際には Zabbix が実際に呼んでいる SQL クエリを tcpdump で抜き出すと手っ取り早い.


以上です.

おまけ

箇条書きで挙動を解説してもわけわからないと思うので,めっちゃ雑に TypeScript で書いたコードを適当に残しておきます.雰囲気を察してください:

  fetchProblems(severity: ZabbixProblemSeverity[]): {}[] {
    return this.jsonRPCClient.doRequest("problem.get", {
      'output': "extend",
      'source': ZabbixProblemSource.createdByTrigger,
      'object': ZabbixProblemObject.trigger,
      'severities': severity,
      'recent': false,
      'sortfield': ["eventid"],
      'sortorder': ["DESC"]
    })['result'];
  }

  fetchEvents(eventIds: string[]): {}[] {
    return this.jsonRPCClient.doRequest("event.get", {
      "output": "extend",
      "eventids": eventIds,
      "selectRelatedObject": "refer",
    })['result'];
  }

  fetchTriggers(triggerIds: string[]): {}[] {
    return this.jsonRPCClient.doRequest("trigger.get", {
      "output": "extend",
      "triggerids": triggerIds,
      "selectItems": "refer",
    })['result'];
  }

  fetchItems(itemIds: string[]): {}[] {
    return this.jsonRPCClient.doRequest("item.get", {
      "output": "extend",
      "itemids": itemIds,
      "selectHosts": "refer",
    })['result'];
  }

  fetchHosts(hostIds: string[]): {}[] {
    return this.jsonRPCClient.doRequest("host.get", {
      "output": "extend",
      "hostids": hostIds,
      "selectHosts": "refer",
      "selectTriggers": "refer",
    })['result'];
  }

  const problems = fetchProblems(this.severity);
  const eventIds = problems.map(p => p['eventid']);

  const events = fetchEvents(eventIds);
  const triggerIds: string[] = [];
  const triggerId2Event: { [triggerId: string]: {} } = {};
  for (const event of events) {
    const triggerId = event['relatedObject']['triggerid'];
    triggerIds.push(triggerId);
    triggerId2Event[triggerId] = event;
  }

  const triggers = fetchTriggers(triggerIds);
  const itemIds: string[] = [];
  const triggerId2Trigger: { [triggerId: string]: {} } = {};
  for (const trigger of triggers) {
    if (trigger['status'] != ZabbixTriggerStatus.enabled) {
      continue;
    }

    const itemId = trigger['items'][0]['itemid'];
    if (!itemId) {
      continue;
    }

    itemIds.push(itemId);
    triggerId2Trigger[trigger['triggerid']] = trigger;
  }

  const items = fetchItems(itemIds);
  const hostIds = items.filter(item => item['status'] == ZabbixItemStatus.enabled)
    .map(item => item['hosts'][0]['hostid'])
    .filter(item => item);

  const hosts = fetchHosts(hostIds);
  const activeProblems: ZabbixActiveProblem[] = [];

  hostLoop:
    for (const host of hosts) {
      if (host['status'] != ZabbixHostStatus.enabled) {
        continue;
      }

      for (const hostTrigger of host['triggers']) {
        const triggerId = hostTrigger['triggerid'];
        const trigger = triggerId2Trigger[triggerId];
        if (!trigger) {
          continue;
        }

        const event = triggerId2Event[triggerId];

        activeProblems.push(new ZabbixActiveProblem(
          host['host'],
          trigger['description'],
          triggerId,
          event['eventid'],
          parseInt(event['severity'], 10),
        ));

        if (activeProblems.length >= MAX_PROBLEMS_NUM) {
          break hostLoop;
        }
      }
    }

*1:この辺が関与してそうだけど深追いはやめた: "This method may return problems of a deleted entity if these problems have not been removed by the housekeeper yet."

*2:

AWSでMFAを利用しつつセッショントークンをいい感じで取ってくるツール "sesstok" を書いた

github.com

書いたと言っても書いたのは3ヶ月近く前でした.リリース告知をしていないことに気づき,本記事にしている次第です。

何をやるツールかというと,以下の記事に書かれていることをいい感じにやるツールでして,

aws.amazon.com

1. Userの accessKeyID, secretAccessKey および mfaSerial (MFA device ARN) を使って、STSのセッショントークンを取ってくる (これらの情報は初回実行時に設定可能で,rcファイルに保存される)
2. 取ってきたセッショントークンをAWSのcredfentialsファイルに適用する (適用せずにトークンだけ取ってくることもできる)

というような動きをします.自動でcredentialsファイルを変更するというのがキーポイントですね.AWSのcredentialsファイルは通常のiniファイルと同等に扱えるので楽です.

f:id:moznion:20191216002002p:plain

こんな感じで sesstok 123456 のような感じでOTP codeを食わせてやるとSTS API経由でセッショントークンが得られて……

f:id:moznion:20191216002356p:plain

AWSのcredentialsファイルがそのセッショントークンに基づいて自動的に変更されるというような感じです.
あとはこのトークンを用いてお好きなようにどやこやするとよいでしょう.


UserがMFAを使っている環境で、なおかつそのUserを特定のRoleにAssume RoleしてAWSを利用するというシチュエーションの時 *1 にいちいちSTSAPIを叩いてcredentialsを適用して……というのがだいぶダルかったためこのようなツールを書いたという経緯です *2
どうぞご利用ください.

*1:つまりUserに対して直接access tokenを紐付けないという運用

*2:その用途についての記述: https://github.com/moznion/sesstok#to-assume-role-for-another-role

golangのstructに対してコンストラクタを自動生成するCLIツール "gonstructor" を書いた

github.com

表題の通りです.出オチのような名前です.

使い方やどういう挙動をするかという話題についてはREADME.mdのSynopsisをご覧いただければと思います.Javalombokをご存知の方はlombok@Valueのような挙動をする,と言えばイメージしやすいでしょう.
基本的にはgo:generateと組み合わせて使うことになるだろうと思います.


モチベーションとしてはREADME.mdのMotivationにも書きましたが,ザックリ言うと以下のような感じです:

  • Structのフィールドをprivateかつimmutableに取り扱いたいことが多い
  • フィールド (ないしはstruct) をimmutableに保つことについて,goでは基本的に「気をつける」しかないが,その際にはフィールドはprivateであるほうが嬉しい
    • フィールドを破壊的に変更できる存在が絞られるのでコントロールしやすくなる
  • フィールドをprivateにすると直接structを作るのが難しくなる
    • 得てしていわゆるコンストラクタのようなものを作りがち
  • structの定義に基づいてコンストラクタを自動で生成したい
    • ついでにフィールドに対するgetterも生えていると便利なことが多いのでそれも自動生成したい

publicなフィールドを行儀悪くグイグイ書き換えるようなことはしないだろ〜,とは思いますが,しかし仕組みとしてできるということはできるという意味なので,そういうのはなるべく仕組み側でやりにくくしてしまうのが良いだろうと考えている次第です.

どうぞご利用ください.


それはそうとしてGitHub Actions便利ですね.

TypeScriptのMap<K, V>をJSON.stringify()に食わせると空のオブジェクトになって困るんですけどって時

(TypeScriptに限らずJavaScriptでもだいたいこのような感じだと思いますが)

例えば以下のようなコードを書くと,出力としては {"body":{}} が得られます.

const body = new Map<string, string>().set("foo", "bar");
console.log(JSON.stringify({body}));

本当は {"body": {"foo": "bar"}} のような構造がほしいところにこれでは困るわけですね.

そこでどうするかと言うと Object.fromEntries() を利用すると良い:

developer.mozilla.org

2019年11月現在だと比較的新しい機能ですね.

const body = new Map<string, string>().set("foo", "bar");
console.log(JSON.stringify({body: Object.fromEntries(body)}));

このようにするとめでたく {"body": {"foo": "bar"}} という構造が得られる.ヤッター!!

これはid:sugyanさんに教えていただきました.ありがとうございます!

なお注意点としては以下のとおりです

  • node 12以降のバージョン (もしくは対応ブラウザ) が必要.MDNのページでサポート状況をご確認ください.
  • tsconfigのlibの指定に es2019 を指定する必要あり

以上です.助かりましたね.

[追記]
JavaScriptの場合について記す (環境はnode v12.13.0).

const body = new Map().set("foo", "bar");
console.log(JSON.stringify({body})); // => {}
const body = new Map().set("foo", "bar");
console.log(JSON.stringify({body: Object.fromEntries(body)})); // => {"body": {"foo": "bar"}}

同じような感じですね.
[追記ここまで]

以下はやり取りのおまけです.

バイナリぽん置きでGPG passphraseベースの暗号化・復号化を行うツールを書いた

表題のとおりです.

github.com

名前は出オチです (ref: エニグマ (暗号機) - Wikipedia)

色々あってgpgコマンドが無い (入れられない) ような環境でもバイト列を暗号化・復号化したいということがあり (あと脳が悪くてgpgコマンドのオプションを覚えられない),バイナリぽん置きでそれをやるツールを書いたという話です.使い方についてはREADMEをご覧ください.至ってシンプル.
もちろんgpgコマンドとの互換性もあるので,gpgでencryptしたファイルをamugineでdecryptしたり,あるいはその逆をすることも可能となっています.

golangはopenpgpの実装をgolang orgで提供しているので大変便利ですね! https://github.com/golang/crypto/tree/master/openpgp

CompletionStage<T>な変数を同期的に処理したいんですけど〜ってとき

Javaの話です.

CompletionStage<T>を返却するようなメソッドがあって,それを同期的に処理したい (単体テストを書くというシーンが最も多いでしょう) ということが生きているとあります.しかしCompletionStageget()のようなメソッドが生えていないので同期的に処理することができません.thenAccept()を使おうにも,例えばJUnitのテストケースではすっぽ抜けてしまってうまく扱えません (逆にテストケース以外ではうまく働くでしょう).

そんなときにどうするかというと,手っ取り早い方法はCompletableFuture<T>にキャストしてしまうことでしょう.すると,オブジェクトにget()が生えてくるので同期的に値を取り扱うことが可能となります.

例:

final CompletionStage<String> completionStage = getSomething();
final CompletableFuture<String> future = (CompletableFuture<String>) completionStage;
final String got = future.get(); // ここで同期的に取り扱える

最初からCompletableFutureで値を返却してほしいですね.以上です.

技術書典7 (く39D) にて「ATコマンドかるた」を頒布します!!!

技術書典7,SORAZINE技術組(く39D*1 にて「ATコマンドかるた」を頒布いたします.

techbookfest.org

「ATコマンドとはなにか?」という疑問については同時に頒布される SORAZINE 技術組 Vol.2019F 中の,おそらく今のところ日本で最も詳しいATコマンドの解説文章をご覧いただければと思いますが *2,その文中から抜粋しますと,

AT コマンドは AT という文字列で始まる、モデムを制御したりするためのテキストベースのコマンド群です.

というものです.ホラ,皆さん生きているとAT+COPSとかAT+CGDCONTとかよく見たり喋ったりするじゃないですか? ソレのかるたを頒布するという話です.

こんな感じ:

f:id:moznion:20190920162109j:plain

これがATコマンドふだで……

f:id:moznion:20190920162156j:plain

これが機能ふだです.

たのしい48種類のATコマンドのふだをご用意してございます! (なお,コマンドふだと機能ふだは左上の通し番号で対応が付いています)

ATコマンドふだを読み上げて機能ふだを取るもよし,機能ふだを読み上げてATコマンドふだを取るもよし,ニヤニヤ眺めるのもよしと三方よしな仕上がりとなっております! これで皆さんも遊びながらにしてATコマンドを暗記できます,やりましたね!!!

f:id:moznion:20190920161953j:plain

f:id:moznion:20190920163632j:plain

このようなかわいいパッケージでお出迎え.

ところで,いまのところこのかるたの印刷代を全て建て替えてるので売れないと年を越せません!!!!!!
技術書典7,SORAZINE技術組(く39D)でお待ちしています!!! よろしくお願いします……


【おわび】

一部ATコマンドふだに誤植がございまして,そちらは大変申し訳ありませんが人力加筆による修正を加えております

(誤)

f:id:moznion:20190920144627j:plain

AT+CCACT

(正: 修正済み)

f:id:moznion:20190920144712j:plain

AT+CGACT

以上です.

*1:よみがながtypoっているのはご愛嬌

*2:ATコマンド学習されたい向きには超おすすめです