Zabbix API+Python3 で複数ホストのデータを一括取得！

この記事はシスコの有志による Cisco Systems Japan Advent Calendar 2021 (2枚目) の 3 日目として投稿しています。
2021年版(1枚目): https://qiita.com/advent-calendar/2021/cisco
2021年版(2枚目): https://qiita.com/advent-calendar/2021/cisco2 (これ)

# はじめに
早いもので 2021 年も年末になりました。一年の締めくくりに、今年のデータ集計などを実施される方もおられるかと思います。Zabbix ではグラフを用いて集計をすることができますが、ホスト数が多いと大変です。そこで、Python3 で Zabbix API を利用し、複数ホストに対してデータを一括取得する簡易スクリプトを作成したので紹介します。ここでは最大トラフィック値を集計してみます。


### 動作結果（例）

```
$ ./zabbixAPI.py 

=== 一括取得スクリプト（期間：2021-10-01〜2021-10-31）===

ホスト名, アイテム名, 最大値 (bps)
R1-xrv9k1, Interface MgmtEth0/RP0/CPU0/0(): Bits received, 5872
R1-xrv9k1, Interface MgmtEth0/RP0/CPU0/0(): Bits sent, 3184
R2-xrv9k2, Interface MgmtEth0/RP0/CPU0/0(): Bits received, 6016
R2-xrv9k2, Interface MgmtEth0/RP0/CPU0/0(): Bits sent, 1952

=== 正常に終了しました===
```

### 動作確認環境
* Zabbix 5.2.7
* Python 3.8.10

### 免責事項
スクリプトの実行は自己責任でお願いします。本スクリプトを実行することにより生じるいかなる問題に関しましても、筆者は一切責任を負いません。



# 使い方
ここではスクリプトの使い方を説明します。

## スクリプトと設定ファイル
まずは下記のスクリプトを適当なところに保存します。ここでは zabbixAPI.py とします。その後、スクリプト中の ZABBIX_URL、ZABBIX_USERNAME および ZABBIX_PASSWORD を環境に合わせて編集します。

<details>
<summary>スクリプト</summary
<div>

```python
#!/usr/bin/env python3

import json
import requests
import time
from datetime import datetime

###################################
# zabbix スクリプト         #
###################################

#####  データ取得期間の指定 #####
START_DATE="2021-10-01"
END_DATE="2021-10-31"

##### zabbix 接続情報  #####
ZABBIX_URL="192.168.1.1"
ZABBIX_USERNAME="Admin"
ZABBIX_PASSWORD="zabbix"

#####  設定ファイルの読み込み #####
# 設定ファイルは、下記のフォーマットになっている必要があります。
#    {
#        "host1": [
#            "item1",
#            "item2"
#        ],
#        "host2": [
#            "item1",
#            "item2"
#        ]
#    
#    }
with open("./config.json","r") as f:
    hosts_items=json.load(f)


# zabbix API の token を取得
def get_token():
    url="http://"+ZABBIX_URL+"/zabbix/api_jsonrpc.php"
    headers={"Content-Type": "application/json-rpc"}
    payload={ "jsonrpc": "2.0",
              "id": "1", 
              "method": "user.login", 
              "params": {
                  "user": ZABBIX_USERNAME, 
                  "password": ZABBIX_PASSWORD }
            }
    res=requests.get(url, headers=headers, data=json.dumps(payload))
    return res.json()["result"]


# hostid を取得
# 使い方：
#   get_host_id(token) -> すべてのホストを取得
#   get_host_id(token, "xrv") -> キーワード "xrv" を含むホストを取得
def get_host_id(token, search=""):
    url="http://"+ZABBIX_URL+"/zabbix/api_jsonrpc.php"
    headers={"Content-Type": "application/json-rpc"}
    payload={ "jsonrpc": "2.0",
              "id": "1", 
              "auth": token,
              "method": "host.get", 
              "params": {
                  #"limit": "3",    # エラー時に表示するアイテム数を制限するときはコメントを削除
                  "output": ["name", "hostid"],
                  "search": {"name": search}
                  }
            }
    res=requests.get(url, headers=headers, data=json.dumps(payload))
    return res.json()


# itemid を取得
# 使い方：
#   get_item_id(token) -> すべてのアイテムを取得
#   get_item_id(token, "Bits") -> キーワード "Bits" を含むアイテムを取得
def get_item_id(token, hostid, search=""):
    url="http://"+ZABBIX_URL+"/zabbix/api_jsonrpc.php"
    headers={"Content-Type": "application/json-rpc"}
    payload={ "jsonrpc": "2.0",
              "id": "1", 
              "auth": token,
              "method": "item.get", 
              "params": {
                  #"limit": "3",    # エラー時に表示するアイテム数を制限するときはコメントを削除
                  "hostids": hostid,
                  "output": ["itemid", "hostid", "name", "key_"],
                  "search": {"name": search}
                  }
            }
    res=requests.get(url, headers=headers, data=json.dumps(payload))
    return res.json()


# trend から最大値を取得
# 使い方：
#   get_max(token, itemid) -> itemid の最大値を取得
def get_max(token, itemid):
    # 開始、終了時間を unix time へ変換
    startpt=datetime.strptime(START_DATE+" 00:00:00", '%Y-%m-%d %H:%M:%S')
    start_unix_time=time.mktime(startpt.timetuple())
    endpt=datetime.strptime(END_DATE+" 23:59:59", '%Y-%m-%d %H:%M:%S')
    end_unix_time=time.mktime(endpt.timetuple())

    url="http://"+ZABBIX_URL+"/zabbix/api_jsonrpc.php"
    headers={"Content-Type": "application/json-rpc"}
    payload={ "jsonrpc": "2.0",
              "id": "1", 
              "auth": token,
              "method": "trend.get", 
              "params": {
                  "itemids": itemid,
                  "output": ["itemid", "clock", "value_max"],
                  "time_from": start_unix_time,
                  "time_till": end_unix_time
                  }
            }
    res=requests.get(url, headers=headers, data=json.dumps(payload))
    return res.json()


# メイン処理
def main():
    print("\n=== 一括取得スクリプト（期間："+START_DATE+"〜"+END_DATE+"）===\n")
    print("ホスト名, アイテム名, 最大値 (bps)")
    token=get_token() # zabbix API token を取得

    for host in hosts_items: # 設定ファイルからホストを一つずつ選択し実行
        # print(host) # debug 用

        ## 01 hostid の取得
        host_id_result=get_host_id(token, host)["result"] # hostId を取得
        if not len(host_id_result)==1:  # 結果が 0 または 2 つ以上の場合、ホスト名が不正のためエラー
            print("\n!!!!!!!!!! エラー !!!!!!!!!!")
            print("ホスト名 "+host+"を正しく取得できませんでした。")
            print("下記の中から正しいホスト名を選択し、設定ファイルに指定してください。")
            print(json.dumps(get_host_id(token, host), indent=2))
            return

        hostname=host_id_result[0]["name"] # 設定ファイルから一意に得られたホスト名
        hostid=host_id_result[0]["hostid"] # hostid
        # print(hostname, hostid) # debug 用

        for item in hosts_items[host]:
            ## 02 itemid の取得
            item_id_result=get_item_id(token, hostid, item)["result"]
            if not len(item_id_result)==1:  # 結果が 0 または 2 つ以上の場合、アイテム名が不正のためエラー
                print("\n!!!!!!!!!! エラー !!!!!!!!!!")
                print("アイテム名 "+item+"を正しく取得できませんでした。")
                print("下記の中から正しいアイテム名を選択し、設定ファイルに指定してください。")
                print(json.dumps(get_item_id(token, hostid, item), indent=2))
                return
            itemid=item_id_result[0]["itemid"]

            ## 03 trends から最大値の取得
            max_result=get_max(token, itemid)["result"]
            if len(max_result)==0:  # 結果が 0 の場合、エラー
                print("\n!!!!!!!!!! エラー !!!!!!!!!!")
                print("  設定を確認してください。")
                print("  現在の設定: 開始日="+START_DATE+"、終了日="+END_DATE+"、itemid="+itemid)
                return

            max_values=[] # value_max を一旦保存しておくリスト
            for val in max_result:
                max_values.append(int(val["value_max"]))  # 全結果を int で数値にしてリストへ
            max_val=max(max_values)  # 最大値を max_val に格納

            print(hostname+", "+item+", "+str(max_val))

    print("\n=== 正常に終了しました===\n")


if __name__=="__main__":
    main()
```
</div>
</details>


## 設定ファイルの作成手順
次に、仮で下記のような設定ファイルを config.json というファイル名で、スクリプトと同じパスに保存します。

```json
$ cat config.json 
{
    "xrv": [
        ""
    ]
}
```

ここで "xrv" には Zabbix に登録されているホスト名を指定します。正確なホスト名が分かっている場合にはそれを指定しますが、もしわからない場合には、一部分だけを指定し、ここで一度スクリプトを実行します。すると、下記のようにエラーとなり、該当するホスト一覧を表示します。


```json
$ ./zabbixAPI.py 

=== スクリプト API （期間：2021-10-01〜2021-10-31）===

ホスト名, アイテム名, 最大値 (bps)

!!!!!!!!!! エラー !!!!!!!!!!
ホスト名 xrvを正しく取得できませんでした。
下記の中から正しいホスト名を選択し、設定ファイルに指定してください。
{
  "jsonrpc": "2.0",
  "result": [
    {
      "hostid": "10410",
      "name": "R1-xrv9k1"
    },
    {
      "hostid": "10411",
      "name": "R2-xrv9k2"
    },
（略）
```

これを参考に、正しいホスト名を指定します。ここでは R1-xrv9k1 を指定することにします。config.json を下記のように書き換えます。

```json
$ cat config.json 
{
    "R1-xrv9k1": [
        ""
    ]
}
```

ここで再度スクリプトを実行すると、今度はアイテムが見つからないため、対象ホストのアイテム一覧をエラーと共に表示します。

```json
$ ./zabbixAPI.py 

=== 一括取得スクリプト（期間：2021-10-01〜2021-10-31）===

ホスト名, アイテム名, 最大値 (bps)

!!!!!!!!!! エラー !!!!!!!!!!
アイテム名 を正しく取得できませんでした。
下記の中から正しいアイテム名を選択し、設定ファイルに指定してください。
{
  "jsonrpc": "2.0",
  "result": [
    {
      "itemid": "35009",
      "hostid": "10410",
      "name": "ICMP ping",
      "key_": "icmpping"
    },
    {
      "itemid": "35010",
      "hostid": "10410",
      "name": "ICMP loss",
      "key_": "icmppingloss"
    },
    {
      "itemid": "35011",
      "hostid": "10410",
      "name": "ICMP response time",
      "key_": "icmppingsec"
    },
（略）
    {
      "itemid": "35315",
      "hostid": "10410",
      "name": "Interface MgmtEth0/RP0/CPU0/0(): Bits received",
      "key_": "net.if.in[ifHCInOctets.3]"
    },
（略）
```

取得したいアイテムを選び、"name" に続くアイテム名をそのまま設定ファイルに記述します。ここでは "Interface MgmtEth0/RP0/CPU0/0(): Bits received" を指定することにします。すると設定ファイルは下記のようになります。


```json
$ cat config.json 
{
    "R1-xrv9k1": [
        "Interface MgmtEth0/RP0/CPU0/0(): Bits received"
    ]
}
```

これでスクリプトを実行すると、アイテムの最大値が得られます。

```
$ ./zabbixAPI.py 

=== 一括取得スクリプト（期間：2021-10-01〜2021-10-31）===

ホスト名, アイテム名, 最大値 (bps)
R1-xrv9k1, Interface MgmtEth0/RP0/CPU0/0(): Bits received, 5872

=== 正常に終了しました===
```

JSON 形式で、下記のように複数ホストまたは複数アイテムを指定することも可能です。


```json
$ cat config.json 
{
    "R1-xrv9k1": [
        "Interface MgmtEth0/RP0/CPU0/0(): Bits received",
        "Interface MgmtEth0/RP0/CPU0/0(): Bits sent"
    ],
    "R2-xrv9k2": [
        "Interface MgmtEth0/RP0/CPU0/0(): Bits received",
        "Interface MgmtEth0/RP0/CPU0/0(): Bits sent"
    ]
}
```

また取得する期間は、スクリプト内の START_DATE および END_DATE を編集して指定します。


# 動作詳細
ここからは動作の詳細を紹介します。Zabbix API は SQL 文と同等の動作をしているため、両者を比べると非常に分かりやすいです。そこで、ここでは Zabbix DB を中心に説明し、対応する Python3 コードを合わせて紹介して行きます。

最大トラフィック値を取得するには下記の 3 つのステップが必要です。

1. ホスト ID の取得
1. アイテム ID の取得
1. アイテムに対する最大トラフィック値の取得

## 1. ホスト ID の取得
まず、Zabbix DB に接続します。

```
$ mysql -u root -p zabbix
Enter password: 
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 271892
Server version: 8.0.26 Source distribution

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
```

ホスト ID は hosts テーブルから取得します。ここでは %xrv9k% とし、文字列 xrv9k を含むホストを一覧で取得してみます（% はワイルドカードを意味します）。

```
mysql> select hostid, host from hosts where host like '%xrv9k%';
+--------+-----------+
| hostid | host      |
+--------+-----------+
|  10410 | R1-xrv9k1 |
|  10411 | R2-xrv9k2 |
|  10412 | R3-xrv9k3 |
|  10413 | R4-xrv9k4 |
|  10414 | R5-xrv9k5 |
|  10415 | R6-xrv9k6 |
+--------+-----------+
6 rows in set (0.01 sec)

mysql> 
```

ここでは R1-xrv9k1 を対象に考え、hostid=10410 で進めます。Zabbix API でも考え方は同じです。下記に該当するコードの抜粋を記載します。

<details>
<summary>Zabbix API でホスト ID を取得</summary
<div>

```python
# hostid を取得
# 使い方：
#   get_host_id(token) -> すべてのホストを取得
#   get_host_id(token, "xrv") -> キーワード "xrv" を含むホストを取得
def get_host_id(token, search=""):
    url="http://"+ZABBIX_URL+"/zabbix/api_jsonrpc.php"
    headers={"Content-Type": "application/json-rpc"}
    payload={ "jsonrpc": "2.0",
              "id": "1", 
              "auth": token,
              "method": "host.get", 
              "params": {
                  #"limit": "3",    # エラー時に表示するアイテム数を制限するときはコメントを削除
                  "output": ["name", "hostid"],
                  "search": {"name": search}
                  }
            }
    res=requests.get(url, headers=headers, data=json.dumps(payload))
    return res.json()
```
</div>
</details>


## 2. アイテム ID の取得
次に取得したいホストのアイテム ID を探します。Zabbix DB では、items テーブルから、そのホストのアイテム一覧を得ることができます。ここでは %Mgmt% とし、Management Interface に関するアイテムのみ取得してみます。


```
mysql> select itemid, hostid, name, key_ from items where hostid=10410 and name like '%Mgmt%';
+--------+--------+---------------------------------------------------------------+--------------------------------------+
| itemid | hostid | name                                                          | key_                                 |
+--------+--------+---------------------------------------------------------------+--------------------------------------+
|  35297 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Inbound packets discarded    | net.if.in.discards[ifInDiscards.3]   |
|  35306 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Inbound packets with errors  | net.if.in.errors[ifInErrors.3]       |
|  35315 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Bits received                | net.if.in[ifHCInOctets.3]            |
|  35324 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Outbound packets discarded   | net.if.out.discards[ifOutDiscards.3] |
|  35333 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Outbound packets with errors | net.if.out.errors[ifOutErrors.3]     |
|  35342 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Bits sent                    | net.if.out[ifHCOutOctets.3]          |
|  35351 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Speed                        | net.if.speed[ifHighSpeed.3]          |
|  35360 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Operational status           | net.if.status[ifOperStatus.3]        |
|  35369 |  10410 | Interface MgmtEth0/RP0/CPU0/0(): Interface type               | net.if.type[ifType.3]                |
+--------+--------+---------------------------------------------------------------+--------------------------------------+
9 rows in set (0.00 sec)

mysql> 
```

ここでは、 Bits received である itemid=35315 について考えて進めます。Zabbix API でも考え方は同じです。下記に該当するコードの抜粋を記載します。

<details>
<summary>Zabbix API でアイテム ID を取得</summary
<div>

```python
# itemid を取得
# 使い方：
#   get_item_id(token) -> すべてのアイテムを取得
#   get_item_id(token, "Bits") -> キーワード "Bits" を含むアイテムを取得
def get_item_id(token, hostid, search=""):
    url="http://"+ZABBIX_URL+"/zabbix/api_jsonrpc.php"
    headers={"Content-Type": "application/json-rpc"}
    payload={ "jsonrpc": "2.0",
              "id": "1", 
              "auth": token,
              "method": "item.get", 
              "params": {
                  #"limit": "3",    # エラー時に表示するアイテム数を制限するときはコメントを削除
                  "hostids": hostid,
                  "output": ["itemid", "hostid", "name", "key_"],
                  "search": {"name": search}
                  }
            }
    res=requests.get(url, headers=headers, data=json.dumps(payload))
    return res.json()
```
</div>
</details>


## 3. アイテムに対する最大トラフィック値の取得
最後に該当するアイテムに対する最大値を取得します。Zabbix では history と trends という 2 つのテーブルでデータを格納しており、それぞれ下記の違いがあります（[参照](https://www.zabbix.com/documentation/2.0/jp/manual/config/items/history_and_trends)）。

* history: 各集取データの値（最大 14 日分）
* trends: 1 時間ごとの平均（最大 5 年分）

ここでは一括取得をしたいので、trends を使用します。

特定期間のアイテムの trends は、下記のようにして取得できます。ここでは 2021 年 10 月 1 日〜 31 日で、itemid=35315 の値を取得します。さらに降順でソートし、最初の 10 アイテムだけを表示しています。

```
mysql> select itemid, from_unixtime(clock), value_max from trends_uint where itemid=35315 and from_unixtime(clock) between '2021-10-1 00:00:00' and '2021-10-31 23:59:59' order by value_max desc limit 10;
+--------+----------------------+-----------+
| itemid | from_unixtime(clock) | value_max |
+--------+----------------------+-----------+
|  35315 | 2021-10-19 18:00:00  |      5872 |
|  35315 | 2021-10-15 15:00:00  |      1528 |
|  35315 | 2021-10-08 15:00:00  |      1520 |
|  35315 | 2021-10-05 10:00:00  |      1320 |
|  35315 | 2021-10-15 16:00:00  |      1280 |
|  35315 | 2021-10-25 04:00:00  |      1272 |
|  35315 | 2021-10-29 19:00:00  |      1272 |
|  35315 | 2021-10-26 22:00:00  |      1272 |
|  35315 | 2021-10-26 03:00:00  |      1264 |
|  35315 | 2021-10-31 01:00:00  |      1256 |
+--------+----------------------+-----------+
10 rows in set (0.00 sec)

mysql>
```

降順でソートしているため、一行目の 5872 bps (=5.872Kbps) が最大値であることがわかりました。実際に Zabbix のグラフを見て、合っているか確認してみます。

![Screen Shot 2021-11-29 at 22.55.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156216/376e11eb-04ac-9ccd-57fd-aafb6a76fec0.png)

グラフでは 5.87Kbps が最大値になっており、同じ値が取得できてることがわかります。Zabbix API でも考え方は同じです。下記に該当するコードの抜粋を記載します。

<details>
<summary>Zabbix API で最大値を取得</summary
<div>

```python
# trend から最大値を取得
# 使い方：
#   get_max(token, itemid) -> itemid の最大値を取得
def get_max(token, itemid):
    # 開始、終了時間を unix time へ変換
    startpt=datetime.strptime(START_DATE+" 00:00:00", '%Y-%m-%d %H:%M:%S')
    start_unix_time=time.mktime(startpt.timetuple())
    endpt=datetime.strptime(END_DATE+" 23:59:59", '%Y-%m-%d %H:%M:%S')
    end_unix_time=time.mktime(endpt.timetuple())

    url="http://"+ZABBIX_URL+"/zabbix/api_jsonrpc.php"
    headers={"Content-Type": "application/json-rpc"}
    payload={ "jsonrpc": "2.0",
              "id": "1", 
              "auth": token,
              "method": "trend.get", 
              "params": {
                  "itemids": itemid,
                  "output": ["itemid", "clock", "value_max"],
                  "time_from": start_unix_time,
                  "time_till": end_unix_time
                  }
            }
    res=requests.get(url, headers=headers, data=json.dumps(payload))
    return res.json()
```
</div>
</details>

ここで一点注意点があります。SQL 文では降順にソートできましたので、簡単に最大値を取得できましたが、API ではこのソートする機能がないようです（Zabbix 5.2.7 の場合）。そのため、一旦全ての trends を Python に取り込み、Python 側で最大値を取得する点が異なります。

最後に、上記の 3 ステップをホスト分ループすれば完成です。これで複数ホストに対して、一括取得が可能になりました。


# 最後に
今回の簡易スクリプトでは、name で itemid を取得しておりますが、もっと良い方法があるかもしれません。またトラフィックだけでなく、サーバリソースなども一括取得する場合はもう少し作り込みが必要になります。細かいツッコミは多々あるかと思いますが、あくまでも簡易スクリプトということでご容赦いただければと思います。皆様、楽しい年末年始をお過ごしください。

# 参考にしたサイト
https://tech-mmmm.blogspot.com/2019/03/zabbixdbsql.html
https://www.zabbix.com/documentation/current/manual/api