zabbix4.0で検知したアラートをtwilioで連続架電する

はじめに

先日作成した、zabbix4.0環境とpythonのtwilio連続架電をつなげようと思います。
出来はまだまだですが、とにかくつなげてそのあと改善していこうと思います。
 
 

前回の記事 その1
前回の記事 その2

 

 

 

zabbix4.0の監視設定

ホストグループを設定。適当に「wordPressServer」



アラートを発生させても良いホストを設定。
私の場合は、このブログのPINGだけブロックしたり戻したりしてテストしました。




アプリケーションを作成。適当に「Ping」




アイテムを設定。ホストやホストグループを設定。
今回はホストに「wordpressserver」。アイテム追加で、以下を作成。




シンプルチェックでPING監視。適当です。




トリガー設定で、「PingCheckTrigger」を作成。



メディアタイプの作成。




以下のように設定。
 名前:twilio_sh
 タイプ:スクリプト
 スクリプト名:twilioKick.sh
 スクリプトパラメータ:{ALERT.SUBJECT} {ALERT.MESSAGE}




メディアタイプのオプションで以下のように設定
 試行回数:1

試行回数を1にする理由は、zabbixがアクションのリトライをすると、
flaskアプリが2重に起動してしまい、動作がおかしくなってしまうためです。





アクションの作成


 

名前:twilio_sh
実行条件:トリガー|等しい|wordpressServer.PingCheckTrigger




 

件名:{HOST.NAME}で障害を検知しました。

メッセージ:
 {EVENT.DATE} {EVENT.TIME} に {HOST.NAME} で、{EVENT.NAME} の障害を検知しました。zabbixの障害IDは {EVENT.ID} です。詳細は、メールをご確認ください。

(※実行内容は、詳細を作り追加すると出来上がる)

実行内容詳細:ユーザに送信「Admin」や「自分で作ったユーザ」
      :次のメディアのみ使用:twilio_sh
      :実行条件:障害確認のステータス 等しい コメントなし


※復旧のアクションも必要に応じて同様に設定を入れます。



 

ユーザー設定へ移動し、Adminや自分で作ったユーザを選択。
その後、メディアの設定。




タイプ:twilio_sh
送信先:使用しないため適当で。
    ※zabbix-twilioプラグインではここに電話番号を入れて飛ばしてましたが、
     アラートごとに連絡先が違ったり連絡先の人ごとに、
     zabbixのユーザを作るのは運用的に非効率なためです。
     (お客様の上長とか、zabbixユーザ作らないですよね。。。)




アクションスクリプトの構成

アクションスクリプトは以下の構成にしました。

/usr/lib/zabbix/alertscripts
 twilioKick.sh    … zabbixからキックされるシェル
 zabbix-twilio.py … twiml(読み上げ内容・ボタン回答処理)を作成し、電話をかけるプログラム
 urlAccess.py    … 電話をかけるため、flaskを動かす契機となるプログラム
 callList.csv     … 電話番号のリスト



 

アクションスクリプトの作成

twilioKick.sh

zabbixサーバへコンソールログインし、以下を実施します。

cd /usr/lib/zabbix/alertscripts
sudo vim twilioKick.sh
#!/bin/bash

#virtualenvの環境変数を定義する
export PATH=$PATH:/usr/lib/zabbix/alertscripts/twilio
export PATH=$PATH:/usr/lib/zabbix/alertscripts/twilio/bin
export PATH=$PATH:/home/ec2-user/.pyenv/shims
export PATH=$PATH:/home/ec2-user/.pyenv/bin
export PATH=$PATH:/usr/local/bin:/usr/bin
export PATH=$PATH:/usr/local/sbin
export PATH=$PATH:/usr/sbin
export PATH=$PATH:/home/ec2-user/.local/bin
export PATH=$PATH:/home/ec2-user/bin

#引数確認
echo 第1引数:$1 >> /usr/lib/zabbix/alertscripts/zabbix-twilio.log 2>&1
echo 第2引数:$2 >> /usr/lib/zabbix/alertscripts/zabbix-twilio.log 2>&1

#virtualenv環境を使用する
source /usr/lib/zabbix/alertscripts/twilio/bin/activate

#zabbixから引数を受けて、zabbix-twilio.pyへ渡す
python /usr/lib/zabbix/alertscripts/zabbix-twilio.py $1 $2 >> /usr/lib/zabbix/alertscripts/zabbix-twilio.log 2>&1 &

#テスト用(引数を固定)
#python /usr/lib/zabbix/alertscripts/zabbix-twilio.py 'テスト件名' 'テスト本文' >> /usr/lib/zabbix/alertscripts/zabbix-twilio.log 2>&1 &

私は、virtualenvを使っているので、環境変数を適用してから、
activateして、pythonを実行しています。
テストをしやすいように引数を固定にしたバージョンもコメントアウトで残しています。




zabbix-twilio.py

# -*- coding: utf-8 -*-

##########ライブラリのインポート##########
from twilio import twiml
from twilio.rest import Client
from twilio.twiml.voice_response import Dial, Number, VoiceResponse, Say, Gather

from time import sleep
from flask import Flask, request, Response
import sys
import urllib.parse
import pandas as pd

import subprocess


##########zabbixアクションタイムアウト回避のため、終了を待たないサブプロセスとして「urlAccess.py」起動###########
subprocess.Popen('python /usr/lib/zabbix/alertscripts/urlAccess.py'.split())

##########twilioアカウント情報##########

#●各自入力● twilioのSIDとauth_tokenを入力
account_sid = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
auth_token = 'b8xxxxxxxxxxxxxxxxxxxxxxxxxxxx'

#●各自入力● 発信元番号
fromNumber='+81xxxxxxxx'

#●各自入力● twiml保存先URL
#ベースURL(ngrokの場合は随時変更) + /say
myurl='https://xxxxxxxxx.ngrok.io/say'


##########インプット収集##########

#メッセージ #zabbix からのアラート情報の受け取り
args = sys.argv
print("件名:" + args[1]) #アラート件名
print("内容:" + args[2]) #アラートメッセージ内容

#zabbixからの値を受け取る({ALERT.SUBJECT})({ALERT.MESSAGE})
sayMessage = 'ザビックスからの障害連絡です。' + args[1] + args[2] + '。以上とな ります。対応する場合は1、を。次のかたに連絡する場合は2、を押してください。'

#テスト用(zabbixから値を受け取らずにTwilioの動作を確認する場合)
#sayMessage = 'ザビックスからのテスト用、障害連絡です。対応する場合は1、を。次 のかたに連絡する場合は2を押してください。'

#連絡先リスト(callList.csv)csvをcallListに格納
callList = pd.read_csv('/usr/lib/zabbix/alertscripts/callList.csv')
print('calList is :')
print(callList)

#連絡先リストの最大行数を取得
call_max = len(callList)
choice = ''
print('call_max is: {}'.format(call_max))

#電話をかける
i = 0
def telCall():
        resp = VoiceResponse()
        print('telCall開始')
        #csvからi番目の電話番号と氏名と会社を取得(電話番号のみ必須)
        global i
        toNumber = (callList.iat[i, 0])
        toName = (callList.iat[i, 1])
        toCorp = (callList.iat[i, 2])

        #twilioへ認証する
        client = Client(account_sid, auth_token)

        #連絡先リストのi番目へ電話発信
        call = client.calls.create(
                to=toNumber,
                from_=fromNumber,
                url=myurl,
                status_callback_event=["queued","in-progress","busy","failed","no-answer","canceled","initiated","ringing","answered","completed"],
                )
        print('call_sid is: {}'.format(call.sid))
        print('順序    : {}人目'.format(i+1))
        print('電話番号  : {}'.format(toNumber))
        print('氏名    : {}'.format(toName))
        print('会社名   : {}'.format(toCorp))

        #コールステータスの取得
        call_status =''

        #ステータス結果が出るまでループ
        while call_status != ('busy' or 'failed' or 'no-answer' or 'canceled' or 'in-progress'):
                call_status = client.calls(call.sid).fetch().status
                print('call_status is: {}'.format(call_status))
                sleep(1)

                #completedの場合は次のループへ進む
                if call_status == 'completed':
                        print("telLoopに戻ります")
                        telLoop()

                #通話中の場合、待機する
                while call_status == 'in-progress':
                        sleep(2)
                        print("通話中…")
                        call_status = client.calls(call.sid).fetch().status

        #最後のステータスを出力
        print('last_call_status is: {}'.format(call_status))

        #もし、相手が電話に出なかった場合、次の連絡先へ
        if call_status == ("busy" or "failed" or "canceled"):
                print("電話に出なかったため、次の連絡先へコールします")

                #電話を切る
                resp.hangup()

                #次の連絡先へ
                i = i + 1
                print ("i = {}".format(i))

                #終了判定
                if i == call_max:
                        print('終了判定:連絡先リスト全てに電話したため、処理を 終了します')
                        sys.exit()
                else:
                        #次の連絡先へ
                        print("telLoopに戻ります")
                        telLoop()



#Flaskアプリ生成
app = Flask(__name__)


#twimlを作る処理
@app.route("/say", methods=['GET', 'POST'])
def say():
        print('/say開始')
        resp = VoiceResponse()

        #gatherで返事を取得し、/gatherへ遷移する
        gather = Gather(num_digits=1, action="/gather",timeout='30')
        gather.say(sayMessage,language='ja-JP', voice='alice')
        resp.append(gather)
        return str(resp)

#電話の相手が「1(対応する)」「2(次の人へ)」を選択した結果で、対応を変える
@app.route("/gather", methods=['GET', 'POST'])
def gather():
        print('/gather開始')
        resp = VoiceResponse()

        #押した番号で分岐
        if 'Digits' in request.values:
                global choice
                global i
                choice = request.values['Digits']

                if choice == '1':
                        #1を押すと、対応者決定により処理終了
                        resp.say('それでは、ご対応お願いします')
                        print('Digits is ' + request.values['Digits'])
                        print('それでは、ご対応お願いします')

                        #電話を切る
                        sleep(3)
                        resp.hangup()

                        #カウントを最大にしてループを止める
                        i = call_max

                        return str(resp)

                elif choice == '2':
                        #2を押すと、次の対応者へ
                        resp.say('次のかたへ連絡します。')
                        print('Digits is ' + request.values['Digits'])
                        print('次のかたへ連絡します。')

                        i = i + 1
                        print ("gather@i = {}".format(i))

                        #電話を切る
                        resp.hangup()

                        #次のループへ
                        resp.redirect("/telLoop")
                        return str(resp)

                else:
                        #何も押さなかったら、再度確認。
                        resp.say("1か2を押してください")
                        print('1か2を押してください')
                        resp.redirect("/say")
                        return str(resp)


@app.route("/telLoop", methods=['GET', 'POST'])
def telLoop():
        global i

        #callList.csvの行数分(i)ループする
        while i <= call_max:
        #終了判定
                if i == call_max:
                        print('終了判定:i = call_max になりましたので処理を終了します')

                        sys.exit()
                else:
                        print('telLoop開始')
                        telCall()



#flaskサーバ起動
if __name__ == "__main__":
        app.run(port=5000,debug=False)

「subprocess.Popen(‘python /usr/lib/zabbix/alertscripts/urlAccess.py’.split())」

1カ所だけサブプロセスにしている箇所があります。
これは、flaskでwebサーバを立ち上げた後、電話をかけるループを開始するために、
URLを開かなくてはいけないのですが、サブプロセスとして起動したら放置。としないと、
zabbixがタイムアウトすることがわかったので、このようにしました。

「app.run(port=5000,debug=False)」

デバッグをoffにしないと、プログラム変更時に自動でサーバ再起動をするので、
Falseにしています。



あとは、前回とあまり変わりありません。


urlAccess.py

# -*- coding: utf-8 -*-

from time import sleep
import urllib.request

#Flaskが起動するまでの間、待機する
sleep(5)

#zabbix-twilio.pyのtelLoopを起動し連続架電を開始する
with urllib.request.urlopen("https://a6ee83e2.ngrok.io/telLoop") as res:
        html = res.read().decode("utf-8")
        print(html)

最初は、シェルでcurlやwgetでバックグラウンド実行できないかなど、
試行錯誤したのですが、結局、zabbixがタイムアウトしてしまうので、
pythonにサブプロセス(終了待ちなし)があったのでこのプログラムを作りました。

callList.csv

電話番号,氏名,会社名
'+8190xxxxxxxx',Aさん,あ社
'+8190xxxxxxxx',Bさん,い社
'+8190xxxxxxxx',Cさん,う社
'+8190xxxxxxxx',Dさん,え社
'+8190xxxxxxxx',Eさん,お社

これは、前回と変わりありません。
zabbix-twilio.pyのループの中で、上から順に① i 行目の電話番号へコールします。




 

動作確認

やり方はいろいろありますが、AWSで実施する場合、
セキュリティグループの有無でアラートを出したり出来ます。




 

セキュリティグループを新規作成し、ICMP – IPv4 を許可する設定にします。




セキュリティグループの変更で、作成したグループをEC2に適用します。
その後、zabbixで監視を開始しセキュリティグループを付け外しすることで、
障害/復旧アラートを検知します。




おわりに

前回挙げていた課題がいくつかクリアできましが、まだうまく動かないところがあります。
 ・たまにzabbix-twilo.pyが終了しないので、アラートの2回目でアクションが起動できない
 ・セキュリティの問題(zabbixユーザでプログラムを起動するので権限付与が難しい)
 ・連続アラート検知時の制御

それでも、なんとか形になってきたかなと思います。
twimlを作る処理をflaskで常駐にして、コールする処理と分けるべきかなど迷いに迷い、
作り直すこともあったのですが、それも全然うまくいかず今の形に落ち着きました。

あまりきれいなプログラムではないので、
アドバイス頂けたら幸いです。

 



コメント

%d人のブロガーが「いいね」をつけました。