Zabbix API の仕組みを理解する


## はじめに

Zabbix は、[Zabbix API](http://www.zabbix.com/jp/zabbix_api.php) を通して操作することができますが、言語毎に [Zabbix API ライブラリ](https://www.zabbix.org/wiki/Docs/api/libraries) が複数種存在しているため、どれを使用すればいいか分かりづらい状態です。加えて、Zabbix API は非常に膨大なメソッド ([Method reference](https://www.zabbix.com/documentation/2.2/manual/api/reference)) とオプションを持つため、各種ライブラリが、そのどれだけを保証範囲とし保守するのかまで突き詰めると選定は非常に困難になるでしょう。

これはあくまで私の持論ですが、全容を把握できない保守の保証もないライブラリを使うくらいならば、時間がかかっても大本を理解するほうがよいと思っています。Zabbix API を使いたいのに、Zabbix API ライブラリに骨を折るのは本末転倒です。

ありがたいことに Zabbix API の仕組みは非常に単純です。この記事では、Zabbix API について説明した後、最小限に Zabbix API を利用する実装を紹介したいと思います。もし、誤りなどがあれば _優しく_ 指摘していただけると幸いです。

## JSON-RPC 2.0

Zabbix API は、要求-応答のプロトコルに [JSON-RPC 2.0](http://www.jsonrpc.org/specification) を使用しています。JSON-RPC は、データ形式に JSON を使用した状態を保持しない軽量の RPC (Remote Procedure Call) プロトコルです。仕様が非常に短いので、仕様を読むことが最も手軽で正しく内容を理解する近道になるでしょう。そのため、ここでは後の説明に必要な部分のみを説明します。

JSON-RPC 2.0 での要求は、`jsonrpc`、`method`、`params`、`id` から構成されます。`jsonrpc` は JSON-RPC のヴァージョン番号で `"2.0"`、`method` は呼び出す手続きの名称、`params` は呼び出した手続きに渡す引数 (省略可)、`id` は要求と応答のひも付けを行うための識別子でクライアント側で決めた値となります。手続名と引数を JSON 形式で送ってやるだけという非常に分かりやすい作りです。

```json:JSON-RPC2.0の要求
{
    "jsonrpc": "2.0",
    "method": "hoge",
    "params": {
        "foo": "bar",
        "spam": "ham"
    },
    "id": 1
}
```

他方、JSON-RPC 2.0 での応答は、`jsonrpc`、`result` もしくは `error`、`id` から構成されます。`jsonrpc` は JSON-RPC のヴァージョン番号で `"2.0"`、`result` は要求が成功した場合の返し値、`error` は要求が失敗した場合のエラー情報、`id` は要求に対応する識別子と同じ値となります。要求同様、結果もしくはエラーが JSON 形式で返ってくるだけという非常に分かりやすい作りです。

```json:成功時のJSON-RPC2.0の応答
{
    "jsonrpc": "2.0",
    "result": "hoge",
    "id": 1
}
```

```json:失敗時のJSON-RPC2.0の応答
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32602,
        "message": "Invalid params",
        "data": "hogehoge"
    },
    "id": 1
}
```

## Zabbix API

Zabbix API と JSON-RPC 2.0 の節での説明で異なるのは、要求に `auth` が含まれる点だけです。`auth` はユーザ認証用のトークンで、Zabbix API における `user.login` メソッドを叩くことで得られます。`user.login` メソッドを叩く時は `auth` は `null` で構いません。

```json:ZabbixAPIにおける要求例(www.zabbix.com/documentation/2.2/manual/apiより)
{
    "jsonrpc": "2.0",
    "method": "user.login",
    "params": {
        "user": "Admin",
        "password": "zabbix"
    },
    "id": 1,
    "auth": null
}
```
```json:ZabbixAPIにおける応答例(www.zabbix.com/documentation/2.2/manual/apiより)
{
    "jsonrpc": "2.0",
    "result": "0424bd59b807674191e7d77572075f33",
    "id": 1
}
```

`user.login` メソッドの返し値としてトークンを得ることができれば、後はそのトークンを `auth` に入れ、JSON-RPC 2.0 の要求-応答の仕組みに則って、やり取りをすれば良いだけです。

```json:authを指定した場合のZabbixAPIにおける要求例(www.zabbix.com/documentation/2.2/manual/apiより)
{
    "jsonrpc": "2.0",
    "method": "host.get",
    "params": {
        "output": [
            "hostid",
            "host"
        ],
        "selectInterfaces": [
            "interfaceid",
            "ip"
        ]
    },
    "id": 2,
    "auth": "0424bd59b807674191e7d77572075f33"
}
```
```json:authを指定した場合のZabbixAPIにおける応答例(www.zabbix.com/documentation/2.2/manual/apiより)
{
    "jsonrpc": "2.0",
    "result": [
        {
            "hostid": "10084",
            "host": "Zabbix server",
            "interfaces": [
                {
                    "interfaceid": "1",
                    "ip": "127.0.0.1"
                }
            ]
        }
    ],
    "id": 2
}
```

## 最小限に Zabbix API を利用する実装

最小限に Zabbix API を利用したい場合、以下のクラスを作っておけば事足ります。インスタンス作成時に認証を行い、トークンをインスタンス内で保持しておき、以降は適当に `method` と `params` を与えて応答を受け取る。応答に、成功時は `result` が、失敗時には `error` が含まれているはずなので分岐させて処理を行う。

```python:最小限にZabbixAPIを利用するコードスニペットとその利用例
# -*- coding: utf-8 -*-

import json
import urllib
import urllib2


class ZabbixApi(object):
    def __init__(self, host, user, password):
        """Zabbix API インスタンスを返す

        :param host: Zabbix サーバの IP アドレス
        :param user: Zabbix API のアクセスユーザ
        :param password: Zabbix API のアクセスユーザパスワード
        :return:
        """
        self.request_id = 1
        self.host = host
        self.auth_token = self.request('user.login', {'user': user, 'password': password})


    def request(self, method, params, auth_token=None):
        """Zabbix API にリクエストを送信する
        id は現行特に必要ないため単純にインクリメントした数値を代入している。

        :param method: Zabbix API のメソッド名
        :param params: Zabbix API のメソッドの引数
        :param auth_token: Zabbix API の認証トークン
        :return: JSON-RPC2.0 形式の応答
        """
        if hasattr(self, 'auth_token'):
            auth_token = self.auth_token
        headers = {"Content-Type": "application/json-rpc"}
        uri = "http://{0}/zabbix/api_jsonrpc.php".format(self.host)
        data = json.dumps({'jsonrpc': '2.0',
                           'method': method,
                           'params': params,
                           'auth': auth_token,
                           'id': self.request_id})
        request = urllib2.Request(uri, data, headers)
        self.request_id += 1
        return json.loads(urllib2.urlopen(request).read())


if __name__ == '__main__':
    api = ZabbixApi(192.168.0.2, 'Admin', 'zabbix')
    response = api.request('host.get', {'output': 'hostid', 'search': {'host': 'hoge'}})
    if 'result' in response:
        pass # 成功時の処理
    elif 'error' in response:
        pass # 失敗時の処理
    else:
        pass # 不具合時の処理
```

実際に Zabbix API ライブラリの幾つかは、上のような内容のコードだけだったりします。ですので、JSON-RPC 2.0 と `auth` を理解できさえすれば、不要なライブラリを導入するのではなく、自身の必要にあった軽量の実装を行ったほうが後々楽になるかもしれません。

## まとめ

この記事では、Zabbix API で使用されている JSON-RPC 2.0 を簡単に説明した後、Zabbix API の仕組みについて触れ、最小限に Zabbix API を利用するための実装を紹介しました。

Zabbix API のドキュメント群は非常に膨大なため、その量に圧倒され、難しいものと思い込んでしまいそうになりますが、いざ腰を据えて見て見ると実に単純で分かりやすいものでした。メソッドにどのような引数を渡せばいいのかは難しかったりするのですが…。

この記事で、Zabbix API へのハードルが下がり、より安心の運用に繋げて頂ければ幸いです。