Zabbix APIあれこれ

Zabbixの公式APIリファレンスが思ってたより不親切というか分かりにくかったので、経験豊富な方から口伝で教わった内容や調べて見つけたことなどをメモします。
(もう少しサンプル増やしてほしいです…)

以下のサンプルは適当にhostgroupで書いていますが、大体同じメソッドでは同様のことができるので適宜読み替えてください。

**※Zabbix 3.0で動作確認しています。**

# 1度のリクエストで複数のリソースを作成・更新・削除したい

更新のドキュメントに載っているサンプルだと、1つのリソースしか更新できなさそうに見えるのですが…。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.update",
  "params": {
    "groupid": "ホストグループID",
    "name": "ホストグループ名"
  },
  "auth": "セッションID",
  "id": 1
}
```

実際のところは`"params"`に配列を渡すことができるので、一括作成・更新・削除が可能です。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.update",
  "params": [
    {
      "groupid": "ホストグループID",
      "name": "ホストグループ名"
    },
    {
      "groupid": "ホストグループID",
      "name": "ホストグループ名"
    },
    {
      "groupid": "ホストグループID",
      "name": "ホストグループ名"
    }
  ],
  "auth": "セッションID",
  "id": 1
}
```

後々になってドキュメントを読み直すと、

> object hostgroup.update(object/**array hostGroups**)

そういうことかと気づかされます…。

# 件数を取得したい

取得したアイテムをプログラム上でカウントしていた時期が筆者にはありました…。

`"countOutput"`パラメーターを使いましょう。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.get",
  "params": {
    "countOutput": true
  },
  "auth": "セッションID",
  "id": 1
}
```

`"result"`にそのままカウントした数値が入ってきます。

```json:sample_result.json
{
    "jsonrpc": "2.0",
    "result": "8",
    "id": 1
}
```

# 完全一致検索したい

取得したアイテムをプログラム上でフィルターしていた時期が(ry

`"filter"`パラメーターを使いましょう。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.get",
  "params": {
    "filter": {
      "name": "Linux servers"
    }
  },
  "auth": "セッションID",
  "id": 1
}
```

```json:sample_result.json
{
  "jsonrpc": "2.0",
  "result": [
    {
      "groupid": "2",
      "name": "Linux servers",
      "internal": "0",
      "flags": "0"
    }
  ],
  "id": 1
}
```

# 部分一致検索したい

取得したアイテムをプログラム上でフィル(ry

`"search"`パラメーターを使いましょう。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.get",
  "params": {
    "search": {
      "name": "server"
    }
  },
  "auth": "セッションID",
  "id": 1
}
```

```json:sample_result.json
{
  "jsonrpc": "2.0",
  "result": [
    {
      "groupid": "2",
      "name": "Linux servers",
      "internal": "0",
      "flags": "0"
    },
    {
      "groupid": "4",
      "name": "Zabbix servers",
      "internal": "0",
      "flags": "0"
    }
  ],
  "id": 1
}
```

# OR検索したい

デフォルトだと`"search"`パラメーターは複数の条件を指定されたときにAND検索を実行します。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.get",
  "params": {
    "search": {
      "name": [
        "server",
        "linux"
      ]
    }
  },
  "auth": "セッションID",
  "id": 1
}
```

```json:sample_result.json
{
  "jsonrpc": "2.0",
  "result": [
    {
      "groupid": "2",
      "name": "Linux servers",
      "internal": "0",
      "flags": "0"
    }
  ],
  "id": 1
}
```

OR検索を行いたいときは`"searchByAny"`パラメーターを`true`に設定しましょう。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.get",
  "params": {
    "search": {
      "name": [
        "server",
        "linux"
      ]
    },
    "searchByAny": true
  },
  "auth": "セッションID",
  "id": 1
}
```

```json:sample_result.json
{
  "jsonrpc": "2.0",
  "result": [
    {
      "groupid": "2",
      "name": "Linux servers",
      "internal": "0",
      "flags": "0"
    },
    {
      "groupid": "4",
      "name": "Zabbix servers",
      "internal": "0",
      "flags": "0"
    }
  ],
  "id": 1
}
```

ちなみに`"filterByAny"`はないみたいです。

# ワイルドカードを使った検索がしたい

`"search"`パラメーターのデフォルトの挙動は`*keyword*`と同様です。

> Return results that match the given wildcard search.
> 
> Accepts an array, where the keys are property names, and the values are strings to search for.
> If no additional options are given, this will perform a LIKE “%…%” search.
> 
> Works only for string and text fields.

これを任意で設定するには`"searchWildcardsEnabled"`パラメーターを`true`に設定します。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.get",
  "params": {
    "search": {
      "name": "linux*"
    },
    "searchWildcardsEnabled": true
  },
  "auth": "セッションID",
  "id": 1
}
```

```json:sample_result.json
{
  "jsonrpc": "2.0",
  "result": [
    {
      "groupid": "2",
      "name": "Linux servers",
      "internal": "0",
      "flags": "0"
    }
  ],
  "id": 1
}
```

# 出力項目を絞りたい

大雑把な筆者は出力項目多くても構わないと思ってしまうのですが。

まめな方は`"output"`パラメーターを使いましょう。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.get",
  "params": {
    "search": {
      "name": "server"
    },
    "output": [
      "name",
      "groupid"
    ]
  },
  "auth": "セッションID",
  "id": 1
}
```

```json:sample_result.json
{
  "jsonrpc": "2.0",
  "result": [
    {
      "groupid": "2",
      "name": "Linux servers"
    },
    {
      "groupid": "4",
      "name": "Zabbix servers"
    }
  ],
  "id": 1
}
```

項目が1つのときは配列じゃなくて大丈夫です。

```json:sample.json
{
  "jsonrpc": "2.0",
  "method": "hostgroup.get",
  "params": {
    "search": {
      "name": "server"
    },
    "output": "groupid"
  },
  "auth": "セッションID",
  "id": 1
}
```

```json:sample_result.json
{
  "jsonrpc": "2.0",
  "result": [
    {
      "groupid": "2"
    },
    {
      "groupid": "4"
    }
  ],
  "id": 1
}
```

# APIをプログラムから叩きたい

ZabbixもPythonも初心者な筆者が見様見真似で書いたコードはこんな感じです。

**※Python 2.7系で動作確認しています。**

```py:zabbix_api.py
#!/usr/bin/python
# vim:fileencoding=utf-8

import urllib2
import json

class ZabbixApi(object):
    """
    Zabbix APIを叩くクラス
    """
    # タイムアウト時間(1分)
    TIMEOUT = 60

    class FailedError(Exception):
        """
        Zabbix APIで何かに失敗したときのエラー
        """
        # エラーメッセージのテンプレート
        ERROR_MESSAGE_TEMPLATE = '"{message}({code}): {data}"'

        def __init__(self, name, reason = None):
            """
            コンストラクタ

            :param name: 失敗したメソッド名
            :param reason: エラーレスポンス
            :return: 自身のインスタンス
            """
            message = 'Failed to {0}.'.format(name)
            if reason is not None:
                print(json.dumps(reason))
                message = ' '.join([message, self.ERROR_MESSAGE_TEMPLATE.format(**reason)])
            super(ZabbixApi.FailedError, self).__init__(message)

    class AuthenticationFailedError(FailedError):
        """
        Zabbixの認証トークン取得に失敗したときのエラー
        """
        def __init__(self, reason = None):
            """
            コンストラクタ

            :param reason: エラーレスポンス
            :return: 自身のインスタンス
            """
            super(ZabbixApi.AuthenticationFailedError, self).__init__('authenticate', reason)

    class DeauthenticationFailedError(FailedError):
        """
        Zabbixの認証トークン破棄に失敗したときのエラー
        """
        def __init__(self, reason = None):
            """
            コンストラクタ

            :param reason: エラーレスポンス
            :return: 自身のインスタンス
            """
            super(ZabbixApi.DeauthenticationFailedError, self).__init__('deauthenticate', reason)

    def __init__(self, host, user_name, password, request_id = 1, encode = 'utf-8'):
        """
        コンストラクタ

        :param host: ZabbixサーバーのIPアドレス
        :param user_name: ユーザー名
        :param password: ユーザーパスワード
        :param request_id: JSON-RPCの要求識別子
        :return: 自身のインスタンス
        """
        self.host = host
        self.user_name = user_name
        self.password = password
        self.request_id = request_id
        self.session_id = None

    def __enter__(self):
        """
        with文に入るときに使用
        
        :return: 自身のインスタンス
        """
        self.authenticate()
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        """
        with文から出るときに使用
        """
        self.deauthenticate()

    def call(self, method, params = {}, through_authenticate = False):
        """
        ZabbixAPIに手続きを要求する

        :param method: Zabbix APIのメソッド名
        :param params: Zabbix APIのメソッドパラメーター
        :param through_authenticate: 事前の認証をパスするか(デフォルトはFalse)
        :return: レスポンスのJSONを解析した辞書
        """
        if not through_authenticate and self.session_id is None:
            self.authenticate()
        uri = 'http://{0}/zabbix/api_jsonrpc.php'.format(self.host)
        body = {
                'jsonrpc': '2.0',
                'method': method,
                'params': params,
                'auth': self.session_id,
                'id': self.request_id
                }
        data = json.dumps(body)
        headers = {'Content-Type': 'application/json-rpc'}
        request = urllib2.Request(uri, data, headers)
        response = urllib2.urlopen(request, timeout = self.TIMEOUT)
        response_json = json.loads(response.read())
        self.request_id = self.request_id + 1
        return response_json

    def authenticate(self):
        """
        ユーザー認証(ログイン)を行う

        :return: Zabbix APIのセッションID
        """
        response = self.call('user.login', {'user': self.user_name, 'password': self.password}, True)
        if 'result' in response:
            self.session_id = response['result']
            return response['result']
        elif 'error' in response:
            raise ZabbixApi.AuthenticationFailedError(response['error'])
        else:
            raise ZabbixApi.AuthenticationFailedError()

    def deauthenticate(self):
        """
        ユーザー認証の解除(ログアウト)を行う
        """
        response = self.call('user.logout', [], True)
        if 'result' in response and response['result']:
            self.session_id = None
        elif 'error' in response:
            raise ZabbixApi.DeauthenticationFailedError(response['error'])
        else:
            raise ZabbixApi.DeauthenticationFailedError()

if __name__ == '__main__':
    import sys

    if len(sys.argv) >= 5:
        host = sys.argv[1]
        user = sys.argv[2]
        password = sys.argv[3]
        method = sys.argv[4]
        params = json.loads(sys.argv[5]) if len(sys.argv) >= 6 else {}
        with ZabbixApi(host, user, password) as api:
            print(json.dumps(api.call(method, params)))
        sys.exit(0)
    else:
        print('Usage: zabbix_api.py host user password method [params]')
        sys.exit(1)
```

vimユーザーなのでマジックコメントの形式はvim形式です。
(他のエディタ使いの皆さんすみません)

実行権限を付与すれば以下のように実行できます。

```shell-session
$ zabbix_api.py host user password hostgroup.get '{"search": {"name": "server"}}'
{"jsonrpc": "2.0", "result": [{"internal": "0", "flags": "0", "groupid": "2", "name": "Linux servers"}, {"internal": "0", "flags": "0", "groupid": "4", "name": "Zabbix servers"}], "id": 2}
```

# 参考

- [Zabbix API の仕組みを理解する](http://qiita.com/Trickey/items/d51d93d525443baa496f)
- [Zabbi APIのJSONフォーマットのサンプルが分かりづらいからメモ](http://qiita.com/blue-emc2-github/items/261c3016ca02f1a959c2)
- [Zabbix Documentation 3.0 - Appendix 1. Reference commentary](https://www.zabbix.com/documentation/3.0/manual/api/reference_commentary#common_get_method_parameters)