LoginSignup
15
18

More than 3 years have passed since last update.

Zabbix APIを操作するコマンドをPython3の標準ライブラリで作る方法

Last updated at Posted at 2019-06-13

あらまし

  • 要望1 Zabbixの初期設定をスクリプトで自動化したい(できれば外部ツールなしで)
  • 要望2 Pythonの勉強をしたい(これから始めるのでPython3で)
  • 要望3 CentOS 7を使いたい

そんなわけで、CentOS 7でZabbixを操作するためのCLIコマンドをPython3の標準ライブラリだけで作ってみました。

現実的には py-zabbixpyzabbixZabbix CLI Tools など既成のライブラリやツールを使った方が早いと思いますが、ZabbixのAPI操作をブラックボックス化したくない人や、既製品への依存を最小化したい人に見てもらえるかもしれないと思い、初めての投稿に挑戦してみました。

実際にZabbixを4.0にバージョンアップした時に自作コマンドが動かなくなったことがありましたが、新旧APIの定義を比較して一部の仕様変更が原因と分かったので、すぐにコマンドを修正して対応することができました。

作った機能

  1. テンプレートとホストをファイルからインポートする
  2. テンプレートとホストを削除する
  3. ホストのマクロを作成する

今回はこれだけですが、Zabbix APIの使い方は統一されているので、どのAPIも同じ方法で操作することができます。

環境の準備

以下、動作を確認したバージョンです。

  • Zabbix 4.0.7
  • Python 3.6.8
  • CentOS 7.6
  • PHP 7.3.1 (5.4.16でも確認済み)
  • MariaDB 10.3.13 (MySQL 5.7.23でも確認済み)

インストール

Zabbix

公式ダウンロードサイトの指示にしたがえば、パッケージのインストールからDBとの接続、PHPの設定まで完了します。

Python3

CentOS 7に元からあるPython2との競合を心配したのですが、実はPython3をインストールするだけで何もしなくても共存できることが分かりました。

# まずEPELリポジトリをインストールする
sudo yum -y install epel-release

# 'python36'と'python36-libs'は依存関係で自動的にインストールされる
sudo yum -y install python36-devel python36-pip

# python3インストール後も、無印の「python」は元からあるPython2にリンクしている
python --version
Python 2.7.5
# python3を使用する時には明示的に「python3」を実行する
python3 --version
Python 3.6.8

予備知識の学習

Zabbix APIの使い方

メソッドの全般的な使い方は、公式の APIのトップページAppendix 1 にまとめられていますが、以下に簡単に説明します。

概要

Zabbix APIはWeb APIで、PHPで動作するWebフロントエンド(Zabbix Web Server)の一部です。プロトコルは JSON-RPC 2.0 を使用します。

  • APIは機能ごとに分割されたメソッドで構成されます。
  • リクエスト/レスポンスともにJSONフォーマットを使用します。

構成

APIは多数のメソッドから構成されており、それぞれ個別のAPI(クラス)にグルーピングされています。例えば host.create というメソッドは host というAPIに属しています。

  • ほとんどのAPIはget, create, update, deleteの4つのメソッドを持っています。

リクエストを実行する

まず全メソッドのリファレンスを開いてください。ページ内のリンクまたは画面左のツリーから、メソッドをグルーピングするAPIを開いて、使いたいメソッドの定義を参照します。

シンプルな例として、'template' API に属する 'template.delete' メソッドの定義を見てください。
各メソッドリファレンスのページは以下のような構成になっています。

  • Description - メソッドの説明
  • Parameters - パラメータの定義
  • Return values - 戻り値の定義
  • Examples - リクエスト/レスポンスのJSONの例
  • Source - メソッドが実装されているPHPソースファイルのパス

Parameters の定義を読むと、配列型で「削除するテンプレートのID」となっています。
テンプレートのIDはZabbixのWeb画面には表示されないようですが、直接データベースを参照すればテンプレート名から検索することができます。

以下は名前が 'Template VM ' で始まるテンプレートのIDを参照する例です。

SELECT hostid, name FROM zabbix.hosts WHERE name LIKE 'Template VM %';
+--------+-------------------------------+
| hostid | name                          |
+--------+-------------------------------+
|  10173 | Template VM VMware            |
|  10174 | Template VM VMware Guest      |
|  10175 | Template VM VMware Hypervisor |
+--------+-------------------------------+
3 rows in set (0.000 sec)

テンプレートなのにhostsテーブルのhostid列を参照している点に疑問を感じると思いますが、Zabbix内部ではテンプレートとホストは同じオブジェクトのように管理されていています。
あとでこの性質を利用して、1つのコマンドでテンプレートとホストの両方を操作できるようにします。

次に Examples のリクエストの例を見てください。

{
    "jsonrpc": "2.0",
    "method": "template.delete",
    "params": [
        "13",
        "32"
    ],
    "auth": "038e1d7b1735c6a5436ee9eae095879e",
    "id": 1
}

最初の"jsonrpc": "2.0"は全メソッド共通で、次の"method":にはAPIメソッド名を指定します。

続く"params":Parameters で定義されているとおりにパラメータを指定します。
template.deleteメソッドのパラメータは「削除するテンプレートIDの配列」と定義されているので、この例のParams: ["13","32"]はIDが1332の2つのテンプレートを削除するという意味です。
先ほどデータベースから見つけた'Template VM VMware Guest'を1つだけ削除する場合は、Params: ["10174"]と指定します。

"auth":には user.loginメソッドで取得する認証トークンを指定します(後述)。
"id":は何でもいいので常に1を指定します。

つまり"params":だけは各メソッドの Parameters で定義されているとおりのパラメータを指定する必要がありますが、それ以外はどのメソッドも同じパターンでJSONリクエストを作ることができです。

戻り値

Return values の定義を読むと、オブジェクト型で「templateidsプロパティの下に削除されたテンプレートのIDを含むオブジェクトを返す」と定義されています。

Examples の戻り値の例を見た方が分かりやすいです。

{
    "jsonrpc": "2.0",
    "result": {
        "templateids": [
            "13",
            "32"
        ]
    },
    "id": 1
}

メソッドの戻り値は、メソッドが正常終了すると"result":で返されます。
この例では"result": {"templateids": ["13","32"]}なので、["13","32"]が削除されたIDの配列で、リクエストで指定された2つのテンプレートの削除に成功したこと分かります。

メソッドでエラーが発生するとレスポンスに"result":がなく、以下の例のように"error":でエラーの詳細が返されます。

{
    "jsonrpc": "2.0",
    "error": {
        "code": -32602,
        "message": "Invalid params.",
        "data": "No groups for host \"Linux server\"."
    },
    "id": 7
}

複雑な例

もう少し複雑な例として、'template.get' メソッドの定義を見てください。

Parameters の定義を見ると、オブジェクト型で「目的の出力を定義するパラメータを指定する」となっていて、このメソッドがサポートするパラメータの一覧表が提示されています。

次に Examples のリクエストの例を見てください。

"params": {
    "output": "extend",
    "filter": {
        "host": [
            "Template OS Linux",
            "Template OS Windows"
        ]
    }
},

"params":にJSONが指定されていて、さらにその中で"output":"filter":が指定されています。これらは Appendix 1. Reference commentary の中の Common "get" method parameters で、「全getメソッドでサポートされているパラメータ」として定義されています。

出力するパラメータの指定が"output": "extend"となっていますが、この"extend"も Appendix 1. Reference commentary の Notation - Data types - query で、「全オブジェクトプロパティを返す」と定義されています。特定の出力プロパティを指定する場合の例は、後述の共通モジュールで実装したメソッドを参照してください。

"filter":の定義も Appendix 1. Reference commentary の中の Common "get" method parameters で説明されています。

Python3の標準ライブラリ

Zabbix APIの操作に必要な主な機能は以下のとおりです。

  • HTTP(S)リクエスト
  • JSONをPOSTメソッドで送信する
  • JSONをパースする

これらは以下の標準ライブラリで実現できることが分かりました。

コマンドの実装

1つの共通モジュールを各コマンドでインポートします。
全部で5つのコマンドを作ったので、計6つのファイルになります。

共通モジュール

コンストラクタと2つのメソッドを持つだけのシンプルなクラスを共通モジュールにしました。

zabbixapilib.py
#!/bin/python3

import sys
import json
import urllib.request


class ZabbixApi:

    REQUEST_HEADER = {"Content-Type": "application/json-rpc"}
    TOKEN_LENGTH = 32

    api_url = ""
    token = ""
    last_request = None
    last_response = None

    def __init__(self, server="http://localhost", auth_token=""):
        """ 
            コンストラクタでAPIのURLを組み立て、与えられれば認証トークンを保存する。
            Args:
                server (string): サーバの 'protocol://host[:port]' を示す文字列(省略可)
                auth_token (string): 'user.login' APIメソッドが返す認証トークン(省略可)
        """

        # APIを呼び出すURLは '/zabbix/api_jsonrpc.php' と定義されている。
        self.api_url = server + '/zabbix/api_jsonrpc.php'

        # 認証トークンとして規定の長さの文字列が指定された場合のみ保存する。
        if len(auth_token) == self.TOKEN_LENGTH:
            self.token = auth_token

    def invoke(self, request_body):
        """ 
            指定されたJSONリクエストでAPIメソッドを呼び出して、レスポンスから戻り値を取り出して返す。

            Args:
                request_body (JSON): リクエストパラメータを含むJSONリクエスト

            Returns:
                JSONレスポンスから 'result' キーで取り出した何か
                型は各APIメソッドの定義に依存する
        """

        # サーバのURL、指定されたJSONリクエスト、ヘッダでPOSTリクエストを生成する
        self.last_request = urllib.request.Request(
            url=self.api_url,
            data=json.dumps(request_body).encode(),
            headers=self.REQUEST_HEADER)

        try:
            # APIを呼び出して生のレスポンスを保存する。
            self.last_response = urllib.request.urlopen(self.last_request)

            # 生レスポンスを read() で文字列型にしてから json.loads() で辞書型に変換する。
            response_map =  json.loads(self.last_response.read())
            # 変換後の辞書オブジェクトから 'result' キーで戻り値を取り出して返す。
            return response_map["result"]
        except:
            sys.stderr.write("\nresponse_map: {0}\n".format(response_map))
            sys.stderr.write("\nrequest_body: {0}\n".format(request_body))
            raise

    def get_object_id(self, object_class, object_name):
        """
            ホストまたはテンプレートの名前からIDを取得して返す。
            いくつかのAPIメソッドでIDが必要になるので共通メソッドとする。

            Args:
                object_class (string): クラス名('host' または 'template')
                object_name (string): ホストまたはテンプレートの名前(例:'Template OS Mac OS X')

            Returns:
                string: ホストまたはテンプレートのID
        """

        # 指定のクラス名から呼び出すAPIメソッド名を組み立てる('host.get' または 'template.get')
        api_method = object_class + ".get"
        # 指定のクラス名から取得パラメータ名を組み立てる('hostid' または 'templateid')
        output_id_key = object_class + "id"

        # 指定された名前のホストまたはテンプレートの配列を取得するJSONリクエストを組み立てる
        request_body = {
            "jsonrpc": "2.0",
            "method": api_method,
            "params": {
                "output": output_id_key,
                "filter": {
                    "host": [
                        object_name
                    ]
                }
            },
            "auth": self.token,
            "id": 1
        }

        try:
            # 共通メソッドを使用してAPIメソッドを呼び出して、戻り値(配列)を取得する。
            object_array = self.invoke(request_body)
            # 指定したホストまたはテンプレートは1つなので、配列の先頭要素を取り出す。
            object_data = object_array[0]
            # 先頭要素からさらに指定した出力パラメータを取り出して返す。
            return object_data[output_id_key]
        except:
            raise

コンストラクタ

引数で受け取ったサーバー情報から、APIのURL(全メソッド共通)を組み立てます。認証トークン(後述)を受け取った場合はフィールドに保存します。

使用例

import zabbixapilib

# コンストラクタにZabbix Webサーバーと認証トークンを指定してインスタンスを生成
api = zabbixapilib.ZabbixApi("http://192.168.56.101", "038e1d7b1735c6a5436ee9eae095879e")

invokeメソッド

引数で受け取ったJSONパラメータからPOSTリクエストを作ってAPIメソッドを呼び出し、戻り値のJSONからresult以下の結果を取り出して呼び出し元に返します。

使用例

request_body = {
    "jsonrpc": "2.0",
    "method": some_api_method,
    "params": [
        some_parameter
    ],
    "auth": api.token,
    "id": 1
}

result_object = api.invoke(request_body)

get_object_idメソッド

一部のAPIメソッドでホスト/テンプレートのIDを指定する必要があるので、ホスト/テンプレートの名前からIDを取得するためのメソッドを用意しました。
内部で使用するAPIメソッドは以下の2つです。

使用例

# ホスト名からホストのIDを取得する。
host_id = api.get_object_id("host", "Zabbix server")

機能別CLIコマンド

以下、どのコマンドも以下のように処理をしています。

  1. 引数を受け取ってJSONパラメータを組み立てる
  2. 共通モジュールのinvokeメソッドを使ってAPIメソッドを呼び出す
  3. 戻り値から必要な値を取り出して標準出力に出力する

バージョンを出力する

Zabbix APIのバージョンを標準出力に出力するだけの簡単なコマンドで、認証も不要です。
動作確認のために作りました。

zbx-api-version
#!/bin/python3

# sys.argv[1] サーバの 'protocol://host[:port]' を示す文字列

import sys
import json
import traceback
import zabbixapilib

request_body = {
    "jsonrpc": "2.0",
    "method": "apiinfo.version",
    "params": [],
    "id": 1
}

api = zabbixapilib.ZabbixApi(sys.argv[1])

try:
    print(api.invoke(request_body))
except:
    sys.stderr.write(traceback.format_exc())
    sys.exit(1)

sys.exit(0)

実行例

# Zabbix APIのバージョンを標準出力に出力する
zbx-api-version 'http://192.168.56.101'
4.0.7

認証トークンを出力する

ユーザ名とパスワードで認証し、成功したら認証トークンを標準出力に出力します。他のコマンドでは認証処理を省略するためにこのコマンドの出力を使用します。

zbx-login
#!/bin/python3

# sys.argv[1] サーバの 'protocol://host[:port]' を示す文字列
# sys.argv[2] ユーザ名
# sys.argv[3] パスワード

import sys
import json
import traceback
import zabbixapilib

api_server = sys.argv[1]
zbx_user = sys.argv[2]
zbx_password = sys.argv[3]

request_body = {
    "jsonrpc": "2.0",
    "method": "user.login",
    "params": {
        "user": zbx_user,
        "password": zbx_password
    },
    "id": 1
}

api = zabbixapilib.ZabbixApi(api_server)

try:
    auth_token = api.invoke(request_body)

    # 取得した文字列の長さが規定どおりの場合のみ成功とする。
    if len(auth_token) != api.TOKEN_LENGTH:
        raise ValueError("Bad length: '{0}'".format(auth_token))

    print(auth_token)
except:
    sys.stderr.write(traceback.format_exc())
    sys.exit(1)

sys.exit(0)

実行例

# ユーザーIDとパスワードでZabbixにログインして認証トークンを取得する
token=$(zbx-login 'http://192.168.56.101' 'Admin' 'zabbix')

echo ${token}
038e1d7b1735c6a5436ee9eae095879e

ホストまたはテンプレートを削除する

ホストまたはテンプレートの名前から内部でIDを取得して削除します。指定されたクラス名('host' または 'template')によって呼び出すAPIメソッドを切り替えます。

zbx-delete-object
#!/bin/python3

# sys.argv[1] サーバの 'protocol://host[:port]' を示す文字列
# sys.argv[2] 'user.login' APIメソッドが返す認証トークン
# sys.argv[3] クラス名('host' または 'template')
# sys.argv[4] ホストまたはテンプレートの名前(例:'Template OS Mac OS X')

import sys
import json
import traceback
import zabbixapilib

api_server = sys.argv[1]
auth_token = sys.argv[2]
object_class = sys.argv[3]
object_name = sys.argv[4]

api = zabbixapilib.ZabbixApi(api_server, auth_token)

try:
    # クラス名と名前からホストまたはテンプレートのIDを取得する。
    object_id = api.get_object_id(object_class, object_name)
except:
    sys.stderr.write(traceback.format_exc())
    sys.exit(1)

# 指定されたクラス名からAPIメソッド名('host.delete' または 'template.delete')を組み立てる。
api_method = object_class + ".delete"

request_body = {
    "jsonrpc": "2.0",
    "method": api_method,
    "params": [
        object_id
    ],
    "auth": api.token,
    "id": 1
}

try:
    response_map = api.invoke(request_body)

    # 指定されたクラス名から出力パラメータ名('hostids' または 'templateids')を組み立てて、
    # 削除されたIDの配列を取得する。
    object_ids = response_map[object_class + "ids"]

    # 要求した削除対象は1つなので、配列の先頭要素を取得する。
    deleted_id = object_ids[0]

    # 削除されたホストまたはテンプレートのIDが要求したIDと同じなら成功とする。
    if deleted_id != object_id:
        raise ValueError("required='{0}', but deleted='{1}'.".format(object_id, deleted_id))

    print("Deleted: " + deleted_id)
except:
    sys.stderr.write(traceback.format_exc())
    sys.exit(1)

sys.exit(0)

実行例

# ユーザーIDとパスワードでZabbixにログインして認証トークンを取得する
token=$(zbx-login 'http://192.168.56.101' 'admin' 'zabbix')

# 「Template VM VMware Guest」という名前のテンプレートを削除する
zbx-delete-object 'http://192.168.56.101' "${token}" 'template' 'Template VM VMware Guest'
Deleted: 10174

ホストまたはテンプレートをファイルからインポートする

XMLまたはJSON形式の設定ファイルからホストまたはテンプレートをインポートします。APIメソッド名が 'host.import' や 'template.import' でないことに注意してください。

zbx-import-config
#!/bin/python3

# sys.argv[1] サーバの 'protocol://host[:port]' を示す文字列
# sys.argv[2] 'user.login' APIメソッドが返す認証トークン
# sys.argv[3] XMLまたはJSON形式の設定ファイルのパス

import sys
import json
import traceback
import zabbixapilib

api_server = sys.argv[1]
auth_token = sys.argv[2]
config_file = sys.argv[3]

# 指定された設定ファイルを開いて全行を読み出す。
try:
    with open(config_file, "r") as opened_file:
        config_lines = opened_file.readlines()
except:
    sys.stderr.write(traceback.format_exc())
    sys.exit(1)

# 各行の前後のブランクを除去して1行に連結する。
config_source = ""
for line in config_lines:
    config_source += line.lstrip().rstrip()

# 1文字目を見てJSONかXMLかを判別する。
if config_source.startswith("{"):
    config_format = "json"
elif config_source.startswith("<"):
    config_format = "xml"
else:
    sys.stderr.write("Unknown format: " + config_lines[0])
    sys.exit(1)

# ファイル内で定義されたオブジェクトがインポートと同時に自動的に生成されるようにしたため
# ルールのパラメータが長くなった。
request_body = {
    "jsonrpc": "2.0",
    "method": "configuration.import",
    "params": {
        "format": config_format,
        "rules": {
            "applications": {
                "createMissing": True,
                # "updateExisting": True,   # Zabbix 4.0で削除されたパラメータ
                "deleteMissing": False
            },
            "discoveryRules": {
                "createMissing": True,
                "updateExisting": True,
                "deleteMissing": False
            },
            "graphs": {
                "createMissing": True,
                "updateExisting": True,
                "deleteMissing": False
            },
            "groups": {
                "createMissing": True
            },
            "hosts": {
                "createMissing": True,
                "updateExisting": True
            },
            "images": {
                "createMissing": True,
                "updateExisting": True
            },
            "items": {
                "createMissing": True,
                "updateExisting": True,
                "deleteMissing": False
            },
            "maps": {
                "createMissing": True,
                "updateExisting": True
            },
            "screens": {
                "createMissing": True,
                "updateExisting": True
            },
            "templateLinkage": {
                "createMissing": True
            },
            "templates": {
                "createMissing": True,
                "updateExisting": True
            },
            "templateScreens": {
                "createMissing": True,
                "updateExisting": True,
                "deleteMissing": False
            },
            "triggers": {
                "createMissing": True,
                "updateExisting": True,
                "deleteMissing": False
            },
            "valueMaps": {
                "createMissing": True,
                "updateExisting": True
            },
        },
        "source": config_source,
    },
    "auth": auth_token,
    "id": 1
}

api = zabbixapilib.ZabbixApi(api_server)

try:
    if api.invoke(request_body) == True:
        print("Imported: '{0}'".format(config_file))
except:
    sys.stderr.write(traceback.format_exc())
    sys.exit(1)

sys.exit(0)

実行例

# ユーザーIDとパスワードでZabbixにログインして認証トークンを取得する
token=$(zbx-login 'http://192.168.56.101' 'admin' 'zabbix')

# 相対パス「conf/VPN Router.xml」の設定ファイルをインポートする
zbx-import-config 'http://192.168.56.101' "${token}" 'conf/VPN Router.xml'
Imported: 'conf/VPN Router.xml'

ホストのマクロを作成する

SNMPコミュニティ名などの定数をマクロとしてホストに設定するためのコマンドです。

host_macro.png

zbx-create-hostmacro
#!/bin/python3

# sys.argv[1] サーバの 'protocol://host[:port]' を示す文字列
# sys.argv[2] 'user.login' APIメソッドが返す認証トークン
# sys.argv[3] ホストの名前(例:'Zabbix server')
# sys.argv[4] マクロ名(例:'{$SNMP_COMMUNITY}')
# sys.argv[5] マクロ値(例:'public')

import sys
import json
import traceback
import zabbixapilib

api_server = sys.argv[1]
auth_token = sys.argv[2]
host_name = sys.argv[3]
macro_name = sys.argv[4]
macro_value = sys.argv[5]

api = zabbixapilib.ZabbixApi(api_server, auth_token)

try:
    # ホストの名前からホストIDを取得する。
    host_id = api.get_object_id("host", host_name)
except:
    sys.stderr.write(traceback.format_exc())
    sys.exit(1)

req_body = {
    "jsonrpc": "2.0",
    "method": "usermacro.create",
    "params": {
        "hostid": host_id,
        "macro": macro_name,
        "value": macro_value
    },
    "auth": auth_token,
    "id": 1
}

try:
    response_map = api.invoke(req_body)

    macro_ids = response_map["hostmacroids"]
    print("Created macro: " + macro_ids[0])
except:
    sys.stderr.write(traceback.format_exc())
    sys.exit(1)

sys.exit(0)

実行例

# ユーザーIDとパスワードでZabbixにログインして認証トークンを取得する
token=$(zbx-login 'http://192.168.56.101' 'admin' 'zabbix')

# 「VPN Router」という名前のホストに、キー名「{$SNMP_COMMUNITY}」値「public」のマクロを登録する
zbx-create-hostmacro 'http://192.168.56.101' "${token}" 'VPN Router' '{$SNMP_COMMUNITY}' 'public'
Created macro: 785

感想

  • 初めてPythonを使用しましたが、Python自体と標準ライブラリおよび公式リファレンスの優秀さに助けられました。また想像以上に少ないコードで実装できたことに驚きました。
  • ZabbixのAPI仕様が統一されていることと、公式リファレンスの優秀さに助けられました。
15
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
18