Mackerel Skillをpythonで作ってみた

なんかAlexa skillを作ってみたかったので、手っ取り早くpythonでやってみた。
ただ残念なことに現時点で招待リクエストを出しているが、未だに反応がないので現物で試せていない。
仕方ないので、現状developerコンソールでの確認で止まっている。
**また今回はMackerel skillなので公開しない、開発用のプライベートスキルとする。**

##1. 今回使うもの

[Zappa](https://github.com/Miserlou/Zappa)

Serverlessのフレームワーク
Lambda + API Gateway を勝手によしなにしてくれる

[flask-ask](https://github.com/johnwheeler/flask-ask)
[Document](http://flask-ask.readthedocs.io/en/latest/requests.html)
Alexaスキルを作るのに、いろいろwrapしてくれる便利なもの

[pyenv](https://github.com/pyenv/pyenv)
python環境構築用

その他クライアント環境
OS      : Mac OS Sierra 10.12.6
aws cli : aws-cli/1.11.182 Python/2.7.10 Darwin/16.7.0 botocore/1.7.40 
python  : 2.7.10

事前にAWSアカウントを取得して、access key / secret key を取得しているとする
*developerアカウントも含む

##2. 準備

pyenvインストール/設定/必要パッケージインストール

```
$mkdir mackerel_skill
$cd mackerel_skill
$sudo pip install virtualenv
$virtualenv venv
$source venv/bin/activate
(venv) jwheeler:mackerel_skill$ pip install flask-ask zappa requests awscli
```
##3. Lambda functionの作成

```mackerel.py
"""
This mackerel skill with buildt mackerel api.
The Intent Schema is "MackerelStatus", Slots is "Hostname".
The Intent Schema is "MackerelChangeStatus", Slots is "Hostname", "Condition".
The Intent Schema is "AlertStatus", Slots is no exist.
"""
import json
import logging
import os
import requests
from operator import itemgetter
from flask import Flask
from flask_ask import Ask, statement


# --------------- Define vars ----------------------------------------

apikey = os.environ['MACKEREL_APIKEY']
mackerel_target_dict = {}

# --------------- Functions that control the skill  ------------------

class MackerelApi():

    def __init__(self, apikey):
        self.mc_id_info = self.get_mc_id(apikey)
        self.hostname_info = self.get_mc_hostname(self.mc_id_info, apikey)
        self.register_host_info(self.mc_id_info, self.hostname_info)
        
    def get_mc_id(self, apikey):
        """
        return mackerel host id list
        """
        headers = {'X-Api-Key': apikey}
        param = {'status' : ['working', 'standby', 'maintenance'] }
        r = requests.get('https://mackerel.io/api/v0/hosts', headers=headers, params=param)
        j_values = r.json()
        host_list = j_values["hosts"]
        host_id_list = []
        for x in  list(host_list):
            host_id_list.append(str(x["id"]))
        return host_id_list

    def get_mc_hostname(self, lt_mc_id, apikey):
        """
        first arg specific mackerel id
        return mackerel host name list
        """
        hos_name_list = []
        for x in list(lt_mc_id):
            headers = {'X-Api-Key': apikey}
	    r = requests.get(('https://mackerel.io/api/v0/hosts/%s' % x ), headers=headers)
            j_values = r.json()
	    host_info = j_values["host"]
            hos_name_list.append(str(host_info["name"]).rstrip(".ppsys.jp"))
        return hos_name_list

    def register_host_info(self, lt_mc_id, lt_host_name):
        """
        register mackerel  { key  hostname : value mackerel_id }
        first arg  mackerel_id list, second arg hostname list
	"""
        merge_list = zip(lt_host_name, lt_mc_id)
        for x, y  in  merge_list:
	    mackerel_target_dict[str(x)] = str(y)
	    mackerel_target_dict[str(y)] = str(x)
        ### all mackerel all id and host name set ###
        mackerel_target_dict["mc_id_all"]  = tuple(lt_mc_id)
        mackerel_target_dict["mc_name_all"] = tuple(lt_host_name)

    def get_mc_status(self, mc_id, apikey):
	"""
	first arg specific mackerel id
        return mackerel host status
        """
        ### all status get param ###
	if str(mc_id) == "all":
            host_status = {}
	    for x in self.get_mc_id(apikey):
	        headers = {'X-Api-Key': apikey}
		r = requests.get(('https://mackerel.io/api/v0/hosts/%s' % x ), headers=headers)
		j_values = r.json()
		host_info = j_values["host"]
                lc_value = host_info["status"]
                lc_key = self.get_value(str(x))
                host_status[lc_key] = lc_value
            return host_status
        else:
            headers = {'X-Api-Key': apikey}
	    r = requests.get(('https://mackerel.io/api/v0/hosts/%s' % mc_id ), headers=headers)
            j_values = r.json()
            host_info = j_values["host"]
            return host_info["status"]

    def get_mc_alert(self, apikey):
        """
        get alert and set alert
        """
        headers = {'X-Api-Key': apikey}
        r = requests.get('https://mackerel.io/api/v0/alerts', headers=headers)
        j_values = r.json()
        alert_list = j_values["alerts"]
        alert_dic = {}
        for x in alert_list:
            if 'hostId' in x:
                alert_dic[str(x['hostId'])] = str(x['status'])
            else:
                alert_dic[str(x['id'])] = str(x['status'])
        return alert_dic

    def change_mc_status(self, mc_id, mc_status, apikey):
        """
        mackerel status change
        first arg specific mackerel id
        second arg status  standby, working, maintenance, poweroff
        """
        ### all change param ###
        if str(mc_id) == "all":
            mackerel_id = self.get_mc_id(apikey)
            for x in mackerel_id:
                payload = {'status': mc_status}
                headers = {'X-Api-Key': apikey, 'content-type': 'application/json'}
                url = ('https://mackerel.io/api/v0/hosts/%s/status' % x )
                r = requests.post(url, data=json.dumps(payload), headers=headers)
        else:
            payload = {'status': mc_status}
            headers = {'X-Api-Key': apikey, 'content-type': 'application/json'}
            url = ('https://mackerel.io/api/v0/hosts/%s/status' % mc_id )
            r = requests.post(url, data=json.dumps(payload), headers=headers)

# --------------- Main handler ------------------


app = Flask(__name__)
ask = Ask(app, '/')
logger = logging.getLogger()


@ask.launch
def launch():
    return mackerel()


@ask.intent("MackerelStatus")
def mackerel_status(Hostname):
    mc = MackerelApi(apikey)
    name = str(Hostname).replace("zero", "0").replace("one", "1").replace("two", "2").strip()
    name = name.decode('utf-8')
    name = name.replace(" ","")
    if str(name) in mackerel_target_dict["mc_name_all"]:
        host_id = mackerel_target_dict[name]
        status_list = mc.get_mc_status(str(host_id), apikey)
        speech = "{host} is {status}" \
            .format(host=name, status=status_list)
    else:
        speech = "{host} is not exist".format(host=name)
    logger.info('speech = {}'.format(speech))
    return statement(speech).simple_card('Hi', speech)


@ask.intent("AlertStatus")
def mackerel_alert():
    mc = MackerelApi(apikey)
    alert_list = mc.get_mc_alert(apikey)
    if alert_list:
        for k, v in alert_list.items():
            if k in mackerel_target_dict:
                speech = 'Now {host} is {condition}'.format(host=k, condition=v)
            else:
                speech = 'Now {condition} is exist'.format(condition=v)
    else:
        speech = "Now all ok"
    logger.info('speech = {}'.format(speech))
    return statement(speech).simple_card('Hi', speech)


@ask.intent("MackerelChangeStatus")
def mackerel_change_status(Hostname, Condition):
    mc = MackerelApi(apikey)
    name = str(Hostname).replace("zero", "0").replace("one", "1").replace("two", "2").strip()
    name = name.decode('utf-8')
    name = name.replace(" ","")
    if str(name) in mackerel_target_dict["mc_name_all"] or str(name) == "all":
        host_id = (lambda target : mackerel_target_dict[name] if str(name) in mackerel_target_dict["mc_name_all"] else "all" )
        target_Condition = str(Condition).decode('utf-8').replace(" ","")
        if target_Condition == "working":
            mc.change_mc_status(host_id(name), "working", apikey)
            speech = "{host} is {status}" \
                .format(host=name, status=target_Condition)
        elif target_Condition == "maintenance":
            mc.change_mc_status(host_id(name), "maintenance", apikey)
            speech = "{host} is {status}" \
                .format(host=name, status=target_Condition)
        elif target_Condition == "standby":
            mc.change_mc_status(host_id(name), "standby", apikey)
            speech = "{host} is {status}" \
                .format(host=name, status=target_Condition)
        elif target_Condition == "poweroff":
            mc.change_mc_status(host_id(name), "poweroff", apikey)
            speech = "{host} is {status}" \
                .format(host=name, status=target_Condition)
        else:
            speech = "{target_Condition} is invalid status".format(target_Condition=target_Condition)
    else:
        speech = "Not found {name}".format(name=name)
    logger.info('speech = {}'.format(speech))
    return statement(speech).simple_card('Hi change status', speech)
```


環境変数をコンソールから設定

"MACKEREL_APIKEY"にmackerel側のaccess keyを設定
KMSを使用する場合は、事前に準備したKMS keyを使用

![スクリーンショット 2017-11-20 15.49.03.png](https://qiita-image-store.s3.amazonaws.com/0/99475/78b8190a-3e4e-32cd-4020-178b53d4dbcf.png)



##3. IAMユーザーの作成/credential設定

ユーザー名 : zappa-mackerel

[Document](http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html)

![スクリーンショット 2017-11-20 14.46.43.png](https://qiita-image-store.s3.amazonaws.com/0/99475/24d62f9e-1193-e88a-aea7-58db5a55b183.png)

![スクリーンショット 2017-11-20 14.47.29.png](https://qiita-image-store.s3.amazonaws.com/0/99475/fee50199-fd07-3bd3-e46d-1e8df2684b9d.png)

![スクリーンショット 2017-11-20 14.47.50.png](https://qiita-image-store.s3.amazonaws.com/0/99475/556fc0bb-224b-18e0-bb93-530b5d7a0638.png)

ユーザー作成後、access key / secret key を書き留める

credential/region設定

```
$aws configure
```

##4. deploy(zappa使用)

```
##initialize
$zappa init
##インタラクティブに設定を聞かれるが、ここではdefaultの設定を使うので"enter"を押せばOK
$zappa deploy dev
##自動的に下記のようなURL(API Gateway)が払い出されるので、書き留める
https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev
```

その他、役に立つコマンド

```
##lambdaを呼び出してエラーになっている箇所を調べるのに利用
$zappa tail dev
```

##4. alexa skill 作成(developer console)


[developer console](https://developer.amazon.com/ja/)

alexaのリストから”カスタム対話モデル”を選び、実際のスキルを作成。
今回は英語で作成。

スキル名   : mackerel skill
呼び出し名 : mackerel skill

今回できる内容は以下の３つ

インテント MackerelStatus -　現在のステータス確認
インテント MackerelChangeStatus -　ホストステータスの変更
インテント AlertStatus -　アラート確認

対象サーバ名
ardb001 / ardb002

サンプル発音の定義によるが、上記形式に合えば”rrap001”とか"rrdb002"とかでも対応される。
ここら辺は、自分の環境に合わせたホスト名でいろいろ設定してみれば対応も広がる。　

![スクリーンショット 2017-11-20 15.15.59.png](https://qiita-image-store.s3.amazonaws.com/0/99475/58b2bd25-65c0-03fd-b566-5e0817ccfe30.png)

対話モデルで作成

![スクリーンショット 2017-11-20 15.17.37.png](https://qiita-image-store.s3.amazonaws.com/0/99475/d5dcfd95-bc73-a492-033b-ee1e8ffc290d.png)

インテントスキーマ

```
{
  "intents": [
    {
      "slots": [
        {
          "name": "Hostname",
          "type": "LITERAL"
        }
      ],
      "intent": "MackerelStatus"
    },
    {
      "slots": [
        {
          "name": "Hostname",
          "type": "LITERAL"
        },
        {
          "name": "Condition",
          "type": "LITERAL"
        }
      ],
      "intent": "MackerelChangeStatus"
    },
    {
      "intent": "AlertStatus"
    }
  ]
}
```

サンプル発話

```
MackerelStatus Host status {a r d b zero zero one|Hostname}
MackerelStatus Host status {a r d b zero zero two|Hostname}
MackerelChangeStatus change status {a r d b zero zero one|Hostname} {working|Condition}
MackerelChangeStatus change status {a r d b zero zero one|Hostname} {power off|Condition}
MackerelChangeStatus change status {a r d b zero zero one|Hostname} {maintenance|Condition}
MackerelChangeStatus change status {a r d b zero zero one|Hostname} {standby|Condition}
MackerelChangeStatus change status {all|Hostname} {working|Condition}
MackerelChangeStatus change status {all|Hostname} {power off|Condition}
MackerelChangeStatus change status {all|Hostname} {maintenance|Condition}
MackerelChangeStatus change status {all|Hostname} {standby|Condition}
AlertStatus mackerel alert
```

エンドポイント設定

![スクリーンショット 2017-11-20 15.23.08.png](https://qiita-image-store.s3.amazonaws.com/0/99475/26cdb07c-b673-37f0-5610-ba3d0f0a26cb.png)

endpointは"HTTPS"を選択し、先ほど払い出されたAPIGatewayのURLを設定

SSL証明書設定

![スクリーンショット 2017-11-20 15.23.28.png](https://qiita-image-store.s3.amazonaws.com/0/99475/b4d6d313-54e6-5946-8bc6-7480e3654609.png)

テスト

アラート確認

![スクリーンショット 2017-11-20 15.28.30.png](https://qiita-image-store.s3.amazonaws.com/0/99475/e70ad497-a9ed-bd58-e992-f8bd275520d4.png)

レスポンス

![スクリーンショット 2017-11-20 15.28.57.png](https://qiita-image-store.s3.amazonaws.com/0/99475/57e1eca1-439a-a6f1-d2ea-87b636146ece.png)

ステータス確認
![スクリーンショット 2017-11-20 15.31.17.png](https://qiita-image-store.s3.amazonaws.com/0/99475/b5185e7f-ef3d-33df-b337-a74db24591a2.png)

ステータス変更(特定のホスト)
![スクリーンショット 2017-11-20 15.32.21.png](https://qiita-image-store.s3.amazonaws.com/0/99475/e5038eb3-b6d2-82f3-bc31-7a4c4faa4b49.png)

ステータス確認(全てのホスト)
![スクリーンショット 2017-11-20 15.33.05.png](https://qiita-image-store.s3.amazonaws.com/0/99475/c6e467c0-9b9e-21a3-3b47-078f4e89aaee.png)


##5. やり残し

やはり、現物のechoが手元にないので試してみないとだめ。
サーバのホスト名とか日本語じゃないので、今回は英語でスキル作成したが日本語も試してみたい。
あとは、mackerelのhost_idとホスト名の取得のために毎度Mackerel側のAPIを叩いているので、サーバ台数が増えたらどこかに保存した方がいいかも(ElastiCacheなど)。