広範囲のネットワークディスカバリについて

「Zabbixで１つのディスカバリリールに広いIPレンジのネットワークディスカバリを設定するとディスカバリに時間がかかる。」こんな問題に遭遇した方も何人かいらっしゃるかと思います。

これはZabbixのネットワークディスカバリが１ディスカバリルールは１ディスカバリプロセスで処理する仕様になっているからです。

その為、ディスカバリプロセスを増やしてもディスカバリ時間は短縮されません。
※：ディスカバリルールが複数ある場合は、ディスカバリプロセスの数を増加させるのは有効です。

<b>上記について下記の改善要望を登録しましたが、今だ投票数0で改善される見通しが立っていません。</b>
[ZBXNEXT-2732 Load balancing of the discovery process](https://support.zabbix.com/browse/ZBXNEXT-2732)

現在の仕様「１ディスカバリルールは１ディスカバリプロセスで処理」でディスカバリ時間を短縮するには、広範囲のIPレンジを細かく分割して登録すれば良いのですが、いちいち細かく計算するのは面倒ですし時間がかかります。

それならZabbixAPIの「drule.create」を使ってIP範囲を分割して登録するスクリプトを作成すれば良いのではないかと考え、IP範囲を分割するロジックを考えたらできてしまいました。

先程のZBXNEXT-2732にも添付してあるのですが、スクリプトはPythonで下記になります。

```py:splite_drule.py
# -*- coding: utf-8 -*- 
import sys, json, urllib2

headers = {"Content-Type":"application/json-rpc"}

# login
def login(usr, pw, url, headers):
	auth_post = json.dumps({'jsonrpc':'2.0', 'method':'user.login', 'params':{'user':usr, 'password':pw}, 'auth':None, 'id': 1})
	req = urllib2.Request(url, auth_post, headers)

	res = urllib2.urlopen(req)
	jdata = json.loads(res.read())
	if "error" in jdata:
		print auth_post
		error = jdata["error"]
		print error["message"]
		print error["code"]
		print error["data"]
		quit()
	return jdata["result"]

# file read
def get_drule(flnm):
	f = open(flnm)
	jdata = json.load(f)
	f.close()
	return jdata

# get name
def get_name(jdata):
	params = jdata["params"]
	return params["name"]

# set params
def set_params(jdata, iprange, name):
	params = jdata["params"]
	params["iprange"] = iprange
	params["name"] = name
	return params

# chk iprange
def chk_iprange(iprange, out):
	if iprange.find("/")==-1:
		print 'Bad IP Range: '+iprange
		print '    "/" does not exist in the string'
		return False
	if iprange.count("/")!=1:
		print 'Bad IP Range: '+iprange
		print '    "/" there is more than one in a string'
                return False

	tmp = iprange.split("/")
	out[0] = tmp[0]
	out[1] = tmp[1]

	if not out[1].isdigit():
		print 'Bad IP Range: '+iprange
		print '    prefix is not numeric'
		return False

	if int(out[1]) <= 0 or int(out[1]) >= 24:
		print 'Bad IP Range: '+iprange
		print '    prefix Range : 0 < prefix < 24'
		return False

	tmp = out[0].split(".")
	out[2] = tmp[0]
	out[3] = tmp[1]
	out[4] = tmp[2]
	out[5] = tmp[3]

	if not out[2].isdigit():
		print 'Bad IP Range: '+iprange
		print '    octet 1 is not numeric'
		return False

	if int(out[2]) < 0 or int(out[2]) > 255:
		print 'Bad IP Range: '+iprange
		print '    octet 1 Range : 0 <= octet 1 <= 255'
		return False

	if not out[3].isdigit():
		print 'Bad IP Range: '+iprange
		print '    octet 2 is not numeric'
		return False

	if int(out[3]) < 0 or int(out[3]) > 255:
		print 'Bad IP Range: '+iprange
		print '    octet 2 Range : 0 <= octet 2 <= 255'
		return False

	if not out[4].isdigit():
		print 'Bad IP Range: '+iprange
		print '    octet 3 is not numeric'
		return False

	if int(out[4]) < 0 or int(out[4]) > 255:
		print 'Bad IP Range: '+iprange
		print '    octet 3 Range : 0 <= octet 3 <= 255'
		return False

	if not out[5].isdigit():
		print 'Bad IP Range: '+iprange
		print '    octet 4 is not numeric'
		return False

	if int(out[5]) < 0 or int(out[5]) > 255:
		print 'Bad IP Range: '+iprange
		print '    octet 4 Range : 0 <= octet 4 <= 255'
		return False

	return True

# chk split
def chk_split(prefix, sp):
	if not sp.isdigit():
		print 'Bad split: ' + sp
		print '    split is not numeric'
		return False

	if int(sp) <= 1 or int(sp) > 24:
		print 'Bad split Range: ' + sp
		print '    split Range : 1 <= split <= 24'
		return False

	if int(sp) <= int(prefix):
		print 'Bad prefix(' + prefix + ') & split(' + sp + ')'
		print '    prefix < split' 
		return False

	return True

# create drule
def create_drule(url, jdata, iprange, sp, name):
	tmp = ["","","","","",""]
	if not chk_iprange(iprange, tmp):
		return False

	ip = tmp[0]
	prefix = tmp[1]
	o1 = tmp[2]
	o2 = tmp[3]
	o3 = tmp[4]
	o4 = tmp[5]

	if not chk_split(prefix, sp):
		return False

	sum = (int(o1) << 24 | int(o2) << 16 | int(o3) << 8 | int(o4) ) & (0xFFFFFFFF - (2 ** (32 - int(sp))))
	sum = sum >> (32-int(prefix))
	sum = sum << (32-int(prefix))

	for i in range(0, 2 << (int(sp) - int(prefix) - 1)):
		a = sum | (i<<8) * (2 ** (24 - int(sp)))
		# split
		o1 = (a & 0xFF000000) >> 24
		o2 = (a & 0x00FF0000) >> 16
		o3 = (a & 0x0000FF00) >> 8
		o4 = 0
		tmp = str(o1) + "." + str(o2) + "." + str(o3) + "." + str(o4)

		jdata["params"] = set_params(jdata, tmp + "/" + sp, name + str(i+1))
		# debug
#		print json.dumps(jdata)

		req = urllib2.Request(url, json.dumps(jdata), headers)
		res = urllib2.urlopen(req)
		ret = json.loads(res.read())
		if "error" in ret:
			print name + str(i+1)
			print tmp
			error = ret["error"]
			print error["message"]
			print error["code"]
			print error["data"]
			return False
	return True
# main
if len(sys.argv) != 7:
        print "Usage : python splite_drule.py <url> <user> <password> <iprange> <split> <json file>"
        quit()

jdata = get_drule(sys.argv[6])
jdata["auth"] = login(sys.argv[2], sys.argv[3], sys.argv[1], headers)
name = get_name(jdata)

ret = create_drule(sys.argv[1], jdata, sys.argv[4], sys.argv[5], name)
if ret:
	print "Ok"
else:
	print "Ng"
```

因みにPythonは殆ど書いたことが無く書き方がPythonっぽくないようです。(笑)
使い方は下記の通りで、url、ユーザ、パスワード、IP範囲、分割範囲、drule.createを書いたjsonファイルを指定します。

```
python splite_drule.py <url> <user> <password> <iprange> <split> <json file>
 
<url>       : http://192.xxx.xx.xxx/zabbix/api_jsonrpc.php
<user>      : admin
<password>  : zabbix
<iprange>   : 192.168.0.0/16
<split>     : 19
<json file> : /tmp/drule.json
```

以下、実行例

この状態で…
![2016-11-26_165910.png](https://qiita-image-store.s3.amazonaws.com/0/103473/64bf4597-1d51-5ddf-3da5-c02f6c61dbc8.png)

以下を実行すると…

```
]# python splite_drule.py http://xxx.xxx.xxx.xxx/zabbix/api_jsonrpc.php admin zabbix 192.111.1.0/16 19 /usr/local/src/dru
le.json
Ok
```

こんな感じにIP範囲が分割されて登録されます。
![2016-11-26_AAA.png](https://qiita-image-store.s3.amazonaws.com/0/103473/a3cffaad-c22d-9101-5661-ba85317dfd1b.png)

実行エラーはこんな感じで出ます。(2重登録のエラーになります。)

```
]# python splite_drule.py http://xxx.xxx.xxx.xxx/zabbix/api_jsonrpc.php admin zabbix 192.111.1.0/16 19 /usr/local/src/drule.json
Zabbix agent discovery1
192.111.0.0
Invalid params.
-32602
Discovery rule "Zabbix agent discovery1" already exists.
Ng
```

上記で使用したdrule.createを書いたjsonファイルは下記になります。

```json:drule.json
{
    "jsonrpc": "2.0",
    "method": "drule.create",
    "params": {
        "name": "Zabbix agent discovery",
        "iprange": "iprange",
        "dchecks": [
            {
                "type": "12"
            }
        ],
        "status" :"1"
    },
    "auth": "auth",
    "id": 1
}
```

例ではICMP pingを使用していますが、他のチェック(FTP、HTTP、…、Zabbixエージェント)でも可能です。

因みに、drule.jsonを一から手で作ると面倒と言う方は元になるディスカバリルールをWebインタフェースから登録して、ZabbixAPIのdrule.get登録したルールを取得して下さい。
取得した結果から不要なパラメータせばOKです。(ReadOnly項目は除外してください。)

詳しくはZabbixAPIの[Discovery check object](https://www.zabbix.com/documentation/2.2/manual/api/reference/dcheck/object#discovery_check)を見て欲しいです。