Help us understand the problem. What is going on with this article?

Mackerel Skillをpythonで作ってみた

More than 1 year has passed since last update.

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

1. 今回使うもの

Zappa

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

flask-ask
Document
Alexaスキルを作るのに、いろいろwrapしてくれる便利なもの

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

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

ユーザー名 : zappa-mackerel

Document

スクリーンショット 2017-11-20 14.46.43.png

スクリーンショット 2017-11-20 14.47.29.png

スクリーンショット 2017-11-20 14.47.50.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

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

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

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

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

対象サーバ名
ardb001 / ardb002

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

スクリーンショット 2017-11-20 15.15.59.png

対話モデルで作成

スクリーンショット 2017-11-20 15.17.37.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

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

SSL証明書設定

スクリーンショット 2017-11-20 15.23.28.png

テスト

アラート確認

スクリーンショット 2017-11-20 15.28.30.png

レスポンス

スクリーンショット 2017-11-20 15.28.57.png

ステータス確認
スクリーンショット 2017-11-20 15.31.17.png

ステータス変更(特定のホスト)
スクリーンショット 2017-11-20 15.32.21.png

ステータス確認(全てのホスト)
スクリーンショット 2017-11-20 15.33.05.png

5. やり残し

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした