twilio + python で連続架電(不在時は次の人へエスカレーション)を行う

はじめに

私はシステムオペレータ出身なので、若いときから夜間コールなどたくさんしており、自動化したいなと常日頃から思っていました。(今は受ける側が多いです)

zabbixのプラグインに「zabbix-twilio」というのがあり、オートコールを実現してくれるのですが、2014年から更新が止まっており、zabbix2系のため、3系だとプログラム修正が多かったので使えずでした。そこで、pythonもtwilioも素人ながら、自分で似たようなものを作ってみました。

今回の記事は、twilioの部分だけです。
後日、zabbixとの連携を書こうと思います。

こんなのが作れたら良いのにな。と思います。




 

準備するもの

私が作った環境は以下のとおりです。
それに限らず、いかようにでも出来ると思います。

・amazon-Linux
・python3.6
・Flask
・ngrok



ngrokの準備

軽量webフレームワークのflaskを使うので、外部とやり取り出来るように、
ngrokを使います。インストール手順は割愛します。

cd /usr/lib/zabbix/alertscripts
./ngrok http 5000

ディレクトリはzabbixとの連携を考えて、上記にしました。
セキュリティ的に、ちょっと問題がありそうなので、いつか改善したいです。

起動すると、以下が表示されます。

Session Status online 
Session Expires 6 hours, 19 minutes
Version 2.2.8
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://xxxxxxxxx.ngrok.io -> localhost:5000
Forwarding https://xxxxxxxxx.ngrok.io -> localhost:5000

印の付いているところをメモしておきます。
ngrokは6時間でセッションが切れてしまうので、
長く使う場合は、無料のユーザ登録をします。


 

連続架電用のコード(zabbix-twilio.py)作成

まだ100%完成ではないのですが、電話をかける部分だけできたので公開します。
不備は随時更新していきます。「もっと良い書き方あるよ!」とか、教えて頂けると嬉しいです。
あと、zabbixとの連携は、まだコメントアウトしています。

cd /usr/lib/zabbix/alertscripts
vim zabbix-twilio.py
< 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


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

#●各自入力● twilioのSIDとauth_tokenを入力
account_sid = 'ACxxxxxxxxxxxxx'
auth_token = 'b8xxxxxxxxxxxxx'

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

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

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

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

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

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

#連絡先リスト(callList.csv)csvをcallListに格納
callList = pd.read_csv('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)

        #通話中の場合、ループ
        while call_status == 'in-progress':
            sleep(2)
            print("telCall@通話中…")
            call_status = client.calls(call.sid).fetch().status

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

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

        #電話を切る
        resp.hangup()

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

        #終了判定
        if i == call_max:
            print('telCall@終了判定:連絡先リスト全てに電話したため、処理を終了します')
            sys.exit()
        else:
            #次の連絡先へ
            print("telCall@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('gather@Digits is ' + request.values['Digits'])
            print('gather@それでは、ご対応お願いします')

            #電話を切る
            sleep(5)
            resp.hangup()
            
            #カウントを最大にしてループを止める
            i = call_max

            return str(resp)

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

            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
    
    while i <= call_max:
    #終了判定
        if i == call_max:
            print('telLoop@終了判定:i = call_max になりましたので処理を終了します')
            
            sys.exit()
        else:
            print('telLoop@開始')
            telCall()

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

連絡先リスト(callList.csv)の作成

電話番号や担当者はよく変わるので、
別ファイルから読み込むことにしました。

cd /usr/lib/zabbix/alertscripts
vim callList.csv
電話番号,氏名,会社名
'+8190xxxxxxxx',Aさん,あ社
'+8190xxxxxxxx',Bさん,い社
'+8190xxxxxxxx',Cさん,う社
'+8190xxxxxxxx',Dさん,え社
'+8190xxxxxxxx',Eさん,お社

実行する

まだ、実運用に耐えられるようなものではなく、全然イケてないのですが、
やり方は以下になります。

①ngrok起動
②https:xxxxの部分を「zabbix-twilio.py」に貼り付ける
③zabbix-twilio.pyを実行
④ブラウザから、https://xxxxxxx.ngrok.io/telLoopを実行(ダサいのでそのうちcurlとかで。。。)
 

実行ログ①(1人目が対応を拒否し、2人目が対応する)

実行ログです。
自分がわかりやすいように、誰に電話し、繋がったか否か、
どんな回答をしたかを追えるようにしたつもりです。

$ python3.6 zabbix-twilio.py
calList is :
電話番号 氏名 会社名
0 '+8190xxxxxxxx' Aさん あ社
1 '+8190xxxxxxxx' Bさん い社
2 '+8190xxxxxxxx' Cさん う社
3 '+8190xxxxxxxx' Dさん え社
4 '+8190xxxxxxxx' Eさん お社
call_max is: 5
Serving Flask app "zabbix-twilio" (lazy loading)
Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
Debug mode: on
Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Restarting with stat
calList is :
電話番号 氏名 会社名
0 '+8190xxxxxxxx' Aさん あ社
1 '+8190xxxxxxxx' Bさん い社
2 '+8190xxxxxxxx' Cさん う社
3 '+8190xxxxxxxx' Dさん え社
4 '+8190xxxxxxxx' Eさん お社
call_max is: 5
Debugger is active!
Debugger PIN: 294-022-087
telLoop@開始
telCall@開始
call_sid is: CAb384df8c05a213bf78e5177f0c6da59e
順序    : 1人目
電話番号  : '+8190xxxxxxxx'
氏名    : Aさん
会社名   : あ社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
/say@開始
127.0.0.1 - - [18/Oct/2018 06:28:24] "POST /say HTTP/1.1" 200 -
call_status is: in-progress
/gather@開始
1か2を押してください
127.0.0.1 - - [18/Oct/2018 06:28:26] "POST /gather HTTP/1.1" 200 -
telCall@通話中…
/say@開始
127.0.0.1 - - [18/Oct/2018 06:28:29] "POST /say HTTP/1.1" 200 -
telCall@通話中…
telCall@通話中…
telCall@通話中…
/gather@開始
gather@Digits is 2
gather@次のかたへ連絡します。
gather@i = 1
127.0.0.1 - - [18/Oct/2018 06:28:35] "POST /gather HTTP/1.1" 200 -
telCall@通話中…
telCall@通話中…
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CAc3a92982b426d8ca3b13748c0f75fc7a
順序    : 2人目
電話番号  : '+8190xxxxxxxx'
氏名    : Bさん
会社名   : い社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
/say@開始
127.0.0.1 - - [18/Oct/2018 06:28:47] "POST /say HTTP/1.1" 200 -
call_status is: in-progress
telCall@通話中…
/gather@開始
gather@Digits is 1
gather@それでは、ご対応お願いします
telCall@通話中…
telCall@通話中…
gather@telLoopに戻ります
telLoop@終了判定:i = call_max になりましたので処理を終了します
telCall@通話中…
telCall@通話中…
telCall@通話中…
telCall@telLoopに戻ります
telLoop@終了判定:i = call_max になりましたので処理を終了します

実行ログ②(全員が電話に出たが、全員対応を拒否した場合)

$ python3.6 zabbix-twilio.py
calList is :
電話番号 氏名 会社名
0 '+8190xxxxxxxx' Aさん あ社
1 '+8190xxxxxxxx' Bさん い社
2 '+8190xxxxxxxx' Cさん う社
3 '+8190xxxxxxxx' Dさん え社
4 '+8190xxxxxxxx' Eさん お社
call_max is: 5
Debugger is active!
Debugger PIN: 294-022-087
telLoop@開始
telCall@開始
call_sid is: CAe55f4f4544050e5cb4b2215b2357ebab
順序    : 1人目
電話番号  : '+8190xxxxxxxx'
氏名    : Aさん
会社名   : あ社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
/say@開始
127.0.0.1 - - [18/Oct/2018 06:26:51] "POST /say HTTP/1.1" 200 -
call_status is: in-progress
telCall@通話中…
/gather@開始
gather@Digits is 2
gather@次のかたへ連絡します。
gather@i = 1
127.0.0.1 - - [18/Oct/2018 06:26:57] "POST /gather HTTP/1.1" 200 -
telCall@通話中…
telCall@通話中…
telCall@通話中…
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CA8ec4286ab4117232e843e03d99cf6bac
順序    : 2人目
電話番号  : '+8190xxxxxxxx'
氏名    : Bさん
会社名   : い社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
/say@開始
127.0.0.1 - - [18/Oct/2018 06:27:10] "POST /say HTTP/1.1" 200 -
call_status is: in-progress
/gather@開始
gather@Digits is 2
gather@次のかたへ連絡します。
gather@i = 2
127.0.0.1 - - [18/Oct/2018 06:27:13] "POST /gather HTTP/1.1" 200 -
telCall@通話中…
telCall@通話中…
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CA1ac63162be6486f3fa52f74414330864
順序    : 3人目
電話番号  : '+8190xxxxxxxx'
氏名    : Cさん
会社名   : う社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
/say@開始
127.0.0.1 - - [18/Oct/2018 06:27:25] "POST /say HTTP/1.1" 200 -
call_status is: in-progress
/gather@開始
gather@Digits is 2
gather@次のかたへ連絡します。
gather@i = 3
127.0.0.1 - - [18/Oct/2018 06:27:27] "POST /gather HTTP/1.1" 200 -
telCall@通話中…
telCall@通話中…
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CAe8969a53a0917571f76727da814c4085
順序    : 4人目
電話番号  : '+8190xxxxxxxx'
氏名    : Dさん
会社名   : え社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
/say@開始
127.0.0.1 - - [18/Oct/2018 06:27:41] "POST /say HTTP/1.1" 200 -
call_status is: in-progress
/gather@開始
gather@Digits is 2
gather@次のかたへ連絡します。
gather@i = 4
127.0.0.1 - - [18/Oct/2018 06:27:43] "POST /gather HTTP/1.1" 200 -
telCall@通話中…
telCall@通話中…
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CA7fba01ac6cd753330f130ca43222e263
順序    : 5人目
電話番号  : '+8190xxxxxxxx'
氏名    : Eさん
会社名   : お社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
/say@開始
127.0.0.1 - - [18/Oct/2018 06:27:55] "POST /say HTTP/1.1" 200 -
call_status is: in-progress
/gather@開始
gather@Digits is 2
gather@次のかたへ連絡します。
gather@i = 5
127.0.0.1 - - [18/Oct/2018 06:27:58] "POST /gather HTTP/1.1" 200 -
telCall@通話中…
telCall@通話中…
telCall@telLoopに戻ります
telLoop@終了判定:i = call_max になりましたので処理を終了します

実行ログ③(全員が電話に出なかった場合)

$ python3.6 zabbix-twilio.py
calList is :
電話番号 氏名 会社名
0 '+8190xxxxxxxx' Aさん あ社
1 '+8190xxxxxxxx' Bさん い社
2 '+8190xxxxxxxx' Cさん う社
3 '+8190xxxxxxxx' Dさん え社
4 '+8190xxxxxxxx' Eさん お社
call_max is: 5
Debugger is active!
Debugger PIN: 294-022-087
telLoop@開始
telCall@開始
call_sid is: CAXXXXXXXXXXXX
順序    : 1人目
電話番号  : '+8190xxxxxxxx'
氏名    : Aさん
会社名   : あ社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: busy
telCall@last_call_status is: busy
telCall@電話に出なかったため、次の連絡先へコールします
i = 1
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CAeda4ec33402a8c5be25760eb4b9d8d1b
順序    : 2人目
電話番号  : '+8190xxxxxxxx'
氏名    : Bさん
会社名   : い社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: busy
telCall@last_call_status is: busy
telCall@電話に出なかったため、次の連絡先へコールします
i = 2
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CAf46d28e3cbcbc0bf6be3656422ca266f
順序    : 3人目
電話番号  : '+8190xxxxxxxx'
氏名    : Cさん
会社名   : う社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: busy
telCall@last_call_status is: busy
telCall@電話に出なかったため、次の連絡先へコールします
i = 3
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CA8fb5e6c6dd9a4d2e8172b19953611644
順序    : 4人目
電話番号  : '+8190xxxxxxxx'
氏名    : Dさん
会社名   : え社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: busy
telCall@last_call_status is: busy
telCall@電話に出なかったため、次の連絡先へコールします
i = 4
telCall@telLoopに戻ります
telLoop@開始
telCall@開始
call_sid is: CA80dbf1742b0354b8f227946bf2e85a67
順序    : 5人目
電話番号  : '+8190xxxxxxxx'
氏名    : Eさん
会社名   : お社
call_status is: queued
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: ringing
call_status is: busy
telCall@last_call_status is: busy
telCall@電話に出なかったため、次の連絡先へコールします
i = 5
telCall@終了判定:連絡先リスト全てに電話したため、処理を終了します

残課題

まだまだ、足りないことがたくさんあって、
実運用には乗せられないと思います。

・自動的にFlaskを終了できない(致命傷)
・複数アラートが発生したときの挙動を確認していない
・セキュリティの問題(公開フォルダにtwilioのIDや連絡先ファイルがある)
・zabbixとの連携

しかしながら、twilioとpythonで連続架電をするコードがネットにあまりなかったので、
大きな成果かなと思いました。引続き、がんばります。

次回はこちらです。






 

参考サイト

コメント

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