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

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

jackson-databind で int の取りうる値を超えた場合の挙動

Javaの話題です.

jackson-databind を使って JSON のデシリアライズを行っていて,数値を int にマッピングしている場合,その値が int (32bit) の取りうる値を超えた時の挙動が「バージョンによって異なって」います.
以下に挙げる挙動は 2.9.3 から 2.9.4 へのアップグレードで変更されています.


jackson-databind-2.9.3 を利用している場合,値が int の範囲を超過するとその値を int にキャストしたもの *1 にデシリアライズされます.

コード:


一方,jackson-databind-2.9.4 で同様のシチュエーションになった時にどうなるかというと,このバージョンからは例外が上げられることになります.

コード:

新たに導入された _convertNumberToInt というメソッドの内部で値が int の取りうる範囲内かどうかを判断し,範囲内でない場合には reportOverflowInt で例外を送出するようになっています.


この変更がどこで入ったのかというと,どうやらこのコミットのようです: Fix #1729 · FasterXML/jackson-databind@6a1152c · GitHub

このコミットログに記されている #1729 がどのようなレポートかと言うと

github.com

「int が範囲外だった時,従来の実装では意図しない値になってしまうので明示的になるように例外を上げてほしい.jackson-core の ParserBase では int が範囲外の際に例外を上げている」という内容のようです.
Author の方も「挙動に一貫性を持たせるためにも,範囲外のときには例外を上げるようにしたほうがよい」と返答をしており,これについては賛成するところです (暗黙的な wraparound は脆弱性の元にもなり得るので).


さて一方で uint32 の範囲でおさまる値 (uint32 の範囲内だと値を負に rewind しても一意になる) を取り扱っているときに,従来の挙動を意識せず利用している場合,予期せぬクラッシュが起こります.
手っ取り早く直すためには int ではなく long として取り扱うというのが良いでしょうが,既存データとの兼ね合いなどの色々な事情によってそうもゆかないこともあるでしょう.そのような際にはカスタムシリアライザ・デシリアライザを書いて乗り切ることになると思います.

import com.fasterxml.jackson.databind.util.StdConverter;

public static class Uint32JacksonSerializer extends StdConverter<Integer, Long> {
    @Override
    public Long convert(Integer n) {
        if (n == null) {
            return 0L; // as you like
        }
        return n.longValue();
    }
}
import com.fasterxml.jackson.databind.util.StdConverter;

public static class Uint32JacksonDeserializer extends StdConverter<Long, Integer> {
    private static final long MAX_UINT32_VALUE = 4294967295L;

    @Override
    public Integer convert(Long n) {
        if (n == null) {
            return 0; // as you like
        }
        if (n > MAX_UINT32_VALUE) {
            throw new IllegalArgumentException("out of the boundary of uint32 value: " + n);
        }
        return n.intValue();
    }
}

このようにカスタム (デ) シリアライザを書いて,

public class JsonClass {
    @JsonSerialize(converter = Uint32JacksonSerializer.class)
    @JsonDeserialize(converter = Uint32JacksonDeserializer.class)
    private Integer foo;
}

などとしてあげると,入れる時・出す時によしなに値を変換して取り扱ってくれるようになります.



しかしこれがパッチバージョンとして入ってくるレベルの変更かというと……大変ですね.まあリリースノートにはしっかり書いてあるのですが……jackson-databind/VERSION-2.x at ae9c91d254954a963cc525e941564c5348181eac · FasterXML/jackson-databind · GitHub

教訓としては「bit長にゆとりを持ってデータ設計をしましょう」「リリースノートをちゃんと読もう」ということです,現場からは以上です.

*1:例えば `2147483648` が来た場合に rewind されて `-2147483648` になる

pcapng ファイルを pcap ファイルに変換する

基本的に Wireshark を使うことで得られるパケットキャプチャファイルは pcapng と呼ばれるフォーマットになっており,これは pcap フォーマットとは異なります.

例えば複数のパケットキャプチャファイルを時系列に従ってマージできるコマンドラインツールである mergecap コマンドは Wireshark が提供しているソフトウェアであるため,これを使って出力される merged なパケットキャプチャファイルはデフォルトでは pcapng フォーマットとなります (ちなみに,これについては mergecap -F pcap というふうに -F オプションでフォーマットを指定すると pcap フォーマットで出力できる).


pcap を前提としたプログラムでこのpcapngファイルを読み込もうとすると往々にしてエラーが起きる (例えば Unknown magic a0d0d0a のようなエラーが上がる) ので,pcapng なファイルを pcap なファイルに変換したくなることがあります.というわけでこうです

tcpdump -r file.pcapng -w file.pcap

こうすることで pcapng ファイルを読み込んで pcap ファイルに変換することができます.簡単ですね.


なおインターネット上を検索してみると tshark を使った方法 (tshark -F pcap -r file.pcapng -w file.pcap) がヒットするのですが,この方法は元のファイルサイズが大きいと Killed という無慈悲なエラーを吐いてコマンドが完遂しないので,特別な理由がない限りは tcpdump を使うのが良いと思います.

AWS CodeBuild で AWS CDK を実行する時に IAM Role に S3 の権限を与えないとハマる

AWS CodeBuild から AWS CDK を呼び出し,いい感じで継続的に構成を更新し続けるパイプラインのようなものを作っておくと何かと便利です.
さてこの時,AWS CodeBuild を実行する IAM Role の権限がそのまま AWS CDK の実行に影響するので,その IAM には

  • CloudFormation Stacks の操作に必要な権限
  • CloudFormation が実際に影響を与える対象の操作 (つまり本当にやりたいこと) に必要な権限

という権限を与えておく必要がありますが,これに加えて

  • CDK の中間生成物をアップロードする S3 Bucket: arn:aws:s3:::cdktoolkit-stagingbucket-* に対する権限
    • s3:*Object
    • s3:ListBucket

も併せて与えないと ❌ YourStack failed: Forbidden: <snip> などというエラーを吐いて死にます.
AWS CDK Forbidden, arn:aws:s3:::cdktoolkit-stagingbucket-* などで検索すると,公式ドキュメントを含めていろいろな情報がヒットするので既知の情報なのでしょう.しかしちょっとハマってしまったのでメモとして記すこととします.

なおタイトルにはこう書いてありますが,これは別に CodeBuild に限った話ではなく,任意の IAM Role で CDK を利用する時には共通して必要です.

シェルスクリプトで空きポートを取ってきたいという時

kazuho さんのこれをシェルスクリプトでやりたいというお話です.

kazuhooku.hatenadiary.org

こんな感じ:

nc -l 0 &
NC_PID=$!
lsof -n -P -p $NC_PID | grep TCP | awk '{print $9}' | awk -F ':' '{print $2}'
kill $NC_PID

nc で ephemeral port を listen して (-l 0),その nc の PID をもとに実際に listen しているポートを引っ張ってくるというシンプルな感じです.

特別な理由がなければ Net::EmptyPort を使えば良い気もします.以上です.

macOS 上で pythonz を使って python をインストールする時に openssl 周りをいい感じにする

macOS 上で pythonz を使って素朴に python をビルドしてインストールすると SSL/TLS 周りでずっこけて基本的に使えないものがインストールされる (そもそも pip とかが動かないので).
brew で openssl を入れている場合は以下のように pythonz install するとうまいこと ssl 周りの native code がコンパイル・リンクされる.

pythonz install --configure="--with-openssl=$(brew --prefix openssl)" 3.7.6

--configure オプションに --with-openssl を渡してやると良い,という感じですね.rbenv などでもこのような対応をしていた記憶がある……

Raspberry Pi を Read-Only Root-FS にする & メモリがいっぱいになったらどうなるのか

Raspberry Pi を Read-Only Root-FS にしておくと急にマシンがダウンした時でもファイルシステムが破損しにくくなります.
もちろん書き込み内容は永続化されなくなりますが (書き込み内容は tmpfs にストアされるので rebootで揮発する),得てして電プチなどの乱暴なオペレーションに晒されがちな Raspberry Pi にはそういった保護機構を入れておくと何かと良いことがあるかと思います.

というわけでコレを使います:
github.com

この repository にあるツールを利用することで,Raspberry Pi をお手軽に Read-Only Root-FS にすることが可能です.
内部的には OverlayFS を利用しており,Write アクセスについては tmpfs に対して操作し, Read アクセスについては SD カードの Read-Only な FS の内容と tmpfs の内容を重ね合わせることによって,所望の動作を実現しているようです.頭がよいですね.

実際のセットアップについては,repository の README.md に書いてある内容をそのまま実行すれば良いです (aptで引っ張ってくる一部ツールに関する記述が欠落しているのでそれを追加で入れる必要あり: initramfs-tools).

環境
# uname -a
Linux 295057330043532 4.19.75-v7+ #1270 SMP Tue Sep 24 18:45:11 BST 2019 armv7l GNU/Linux
# cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
NAME="Raspbian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
Installation

基本的に README.md と同じですが,念の為メモ程度に残しておくこととする (オリジナルのドキュメントを参照することを強く勧めます):

sudo su
apt update
apt install -y git rsync gawk busybox bindfs initramfs-tools
dphys-swapfile swapoff
dphys-swapfile uninstall
update-rc.d dphys-swapfile disable
systemctl disable dphys-swapfile
git clone https://github.com/josepsanzcamp/root-ro.git
rsync -va root-ro/etc/initramfs-tools/* /etc/initramfs-tools/
mkinitramfs -o /boot/initrd.gz
echo initramfs initrd.gz >> /boot/config.txt
reboot
Swap が Disabled になっているかどうか見る
pi@raspberrypi:~ $ free
              total        used        free      shared  buff/cache   available
Mem:         443080       32452      199196        5904      211432      352712
Swap:        102396           0      102396
pi@raspberrypi:~ $ free
              total        used        free      shared  buff/cache   available
Mem:         443080       30756      351780        1504       60544      359840
Swap:             0           0           0

Disabled になってますね.

Read-Only になってるかどうかの確認
echo "howdy?" >> am_i_here
sudo reboot
# After a while...
ls am_i_here
# ^ Should be missing
Swap が disabled && Read-Only Root-FS 環境下でめちゃ write されたときの挙動

ちょっとどうなるか気になったので検証.

pi@raspberrypi:~ $ free
              total        used        free      shared  buff/cache   available
Mem:         443080       31296      383472        2240       28312      371056
Swap:             0           0           0

dd で書いてみる:

pi@raspberrypi:~ $ dd if=/dev/zero of=tmpfile bs=314572800 count=1
Killed
pi@raspberrypi:~ $ echo $?
137

Out of memory っぽいエラーコード

1 MiB ずつファイルに書き込むスクリプトで検証:

#!/usr/bin/env perl

use strict;
use warnings;
use utf8;

open my $fh, '>>', './tmp' or die $!;

for (my $i = 0; $i <= 400; $i++) {
    # puts 1 Mib each iteration
    print $fh 'x' x (1024 * 1024) or die "$i MiB: $!";
}
__END__
$ perl check.pl
215 MiB: No space left on device at check.pl line 11.
Warning: unable to close filehandle $fh properly: No space left on device at check.pl line 11.

Disk full と同じような挙動に見えますね.

free してみる:

$ free
              total        used        free      shared  buff/cache   available
Mem:         443080       31496      158672      223020      252912      148164
Swap:             0           0           0

shared が増えて,free及びavailableが減少していますね.

もう一度スクリプトを実行してみる:

$ perl check.pl
0 MiB: No space left on device at check.pl line 11.
Warning: unable to close filehandle $fh properly: No space left on device at check.pl line 11.

今度は書き込めずに Disk Full エラーが返却されていますね.

その一方で free 領域はある程度残っており,マシンがハングアップしてにっちもさっちもいかなくなる,という状況にはならないようです.こういった挙動はおそらく tmpfs 自体の挙動でしょう.

とはいえ swap を切っているので,実際にプロセスがメモリを食い切ると OOM Killer が走ったり,最悪マシンがハングしてしまうと思います.継続的なメモリ使用量の監視が必要かもしれません.そもそもファイルをもりもり書くタイプのワークロードには Read-Only な Root-FS は不向きな気がしました.

以上です.

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: