Google Home MiniにPrometheusのアラートを喋らせてみた

## はじめに
職場の先輩から、昔はエヴァンゲリオンのアスカの声で喋るハブがあったという話を聞いたので、Google Home Miniで似たようなものを作ってみようという試みです。
今回は家のネットワーク機器のインターフェースを監視し、up/downが切り替わった時に喋ってもらいます。


## 構成
監視はPrometheusで行うことにしました。
触ったことがなかったのでいい機会かなと。

![topo.jpeg](https://qiita-image-store.s3.amazonaws.com/0/223892/ade1a6ba-5cec-31b0-0a0b-eb1ade2c33db.jpeg)

コンポーネントごとにdockerコンテナを作ります。
各設定ファイル(とソースコード)はホストからマウントさせることにしました。
dockerfileと設定ファイルを作ってdocker-composeでまとめます。

## 構築
検証環境
- macOS Sierra 10.12.6
- docker for mac : Docker Community Edition 17.09.1-ce-mac42

監視対象
- Cisco 891FJ : GigabitEthernet0-7


### SNMP Exporter
Prometheusは各Exporterから監視対象の情報を取得します。
今回はSNMPポーリングによって監視を行うのでSNMP Exporterを使用します。
Prometheusのアラートのルールを記述するためにこちらで先にメトリックを定義しておきます。

ベースはCentOSのコンテナを使用することにしました。
Prometheusのコンポーネントはバイナリをダウンロードして実行で済むので構築も楽ですね。
[ここ](https://github.com/prometheus/snmp_exporter/releases)から対応のバイナリをダウンロードします。
あとは展開して実行するだけです。(バージョンは現時点で最新のものを使用します。)

```:dockerfile_snmp
FROM centos

RUN yum -y update
RUN yum -y install wget
RUN wget https://github.com/prometheus/snmp_exporter/releases/download/v0.8.0/snmp_exporter-0.8.0.linux-amd64.tar.gz
RUN tar xvf snmp_exporter-0.8.0.linux-amd64.tar.gz

WORKDIR /snmp_exporter-0.8.0.linux-amd64

CMD ./snmp_exporter
```
続いてSNMP Exporterのバイナリと同じディレクトリにある設定ファイル(snmp.yml)に、snmpwalkとmetricの情報を記述します。

IOSのInterfaceのup/downを監視するには以下のMIBオブジェクトを使用します。
1.3.6.1.2.1.2.1.8
試しにsnmpwalkを実行。

```console
~ ❯❯❯ snmpwalk -v 2c -c public Cisco891FJ 1.3.6.1.2.1.2.2.1.8
(省略)
IF-MIB::ifOperStatus.6 = INTEGER: up(1)
IF-MIB::ifOperStatus.7 = INTEGER: down(2)
(省略)
```
ちなみに末尾の数字とInterfaceの名称の対応は以下で取得できました。

```console
~ ❯❯❯ snmpwalk -v 2c -c public Cisco891FJ 1.3.6.1.2.1.2.2.1.2
(省略)
IF-MIB::ifDescr.6 = STRING: GigabitEthernet0
IF-MIB::ifDescr.7 = STRING: GigabitEthernet1
(省略)
```
GigabitEthernet0-7の監視をするためには1.3.6.1.2.1.2.2.1.8.6 - 1.3.6.1.2.1.2.2.1.8.13のオブジェクトが参照できれば問題なさそうです。

SNMP Exporterにはsnmpwalkの情報やメトリックの定義をモジュール単位で記述し、監視対象の情報はPrometheusの設定に記述します。

```yml:snmp.yml
interface:
  version: 2
  auth:
    community: public
  walk:
  - 1.3.6.1.2.1.2.2.1.8
  metrics:
  - name: ifOperStatus
    oid: 1.3.6.1.2.1.2.2.1.8
    type: gauge
    indexes:
      - labelname: ifNumber
        type: Integer32
    help: The current operational status of the interfaces. - 1.3.6.1.2.1.2.2.1.8
```

SNMP Exporterは単体でも定義したモジュールのテストができます。
![スクリーンショット 2018-01-01 0.17.26.png](https://qiita-image-store.s3.amazonaws.com/0/223892/89acdc53-6c88-9270-e0a4-ade3f97caf13.png)



### Prometheus
すでにコンテナが用意されていたので使わせてもらいました。
よってdockerfileは不要。


コンテナの/etc/prometheus/にPrometheusの設定ファイルとalertのルールを定義したファイルを配置します。
公式のサンプルに手を加えて作成しました。
Alertmanagerとrule_filesの指定、snmpのjobの定義を記述します。

```yml:prometheus.yml
# my global config
global:
  scrape_interval:     1s
  evaluation_interval: 1s

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alert_manager:9093 # alert managerのIPアドレス : 待ち受けポート

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  - "alert.rules.yml" # alertのruleを記述したファイルの指定


# A scrape configuration containing exactly one endpoint to scrape:
scrape_configs:
  - job_name: 'snmp'
    static_configs:
      - targets:
        - x.x.x.x  # SNMPの監視対象のIPアドレス
    metrics_path: /snmp
    params:
      module: [interface]
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: snmp_exporter:9116  # snmp exporterのIPアドレス : 待ち受けポート
```

アラートのルールはdelta()で10秒間(始点と終点)の状態の差を計算し、0でなければ状態が遷移したとみなします。
10秒間にしたのは気分。

```yml:alert.rule.yml
groups:
- name: interface
  rules:
  - alert: Interface_Down
    expr: delta(ifOperStatus{ifNumber="6"}[10s]) > 0
    for: 1s
    annotations:
      summary: 891fj/インターフェース/0/ダウン
  - alert: Interface_Down
    expr: delta(ifOperStatus{ifNumber="7"}[10s]) > 0
    for: 1s
    annotations:
      summary: 891fj/インターフェース/1/ダウン
  - alert: Interface_Down
  (省略)
```
省略した部分には各インターフェスのruleを記述しました。
Upの場合は`delta(ifOperStatus{ifNumber="x"}[10s]) < 0`になります。
(プログラマブルに記述する方法あるんですかね？)


summaryの内容がGoogle Home Miniに発話させる内容です。
「/」は音声に区切りを与えるためです。



### Alertmanager
Alertmanagerもバイナリをダウンロードして実行するだけです。
SNMP Exporterと同じ要領でさくっとdockerfileを書きます。
バイナリは[ここ](https://prometheus.io/download/)から探します。

```:dockerfile_alert
FROM centos

RUN yum -y update
RUN yum -y install wget
RUN wget https://github.com/prometheus/alertmanager/releases/download/v0.12.0/alertmanager-0.12.0.linux-amd64.tar.gz
RUN tar xvf alertmanager-0.12.0.linux-amd64.tar.gz

WORKDIR /alertmanager-0.12.0.linux-amd64

CMD ./alertmanager
```

設定ファイルはバイナリと同じディレクトリにあります。
Google Home Notifierに対してJSONをPOSTするように設定を記述します。

```yml:alertmanager.yml
global:

route:
  receiver: 'webhook'
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 1s
  group_interval: 5s
  repeat_interval: 5s

receivers:
- name: 'webhook'
  webhook_configs:
  - send_resolved: false
    url: 'http://notifier:8091/google-home-notifier'
```
アラートは10秒間出続けるはずですが、slack通知の場合7秒程度時間がかかり、実際には同じ通知は3秒間（3回）しか飛びませんでした。
それを参考に同一のアラート通知に関するrepeat_intervalは5秒にしました。
group_intervalも同じく5秒にしました。
今回は全てのアラートが同一グループになるので、例えばGigabitEthernet 0がアップしてから2秒後にGigabitEthernet 1がアップしても、通知は5秒間のインターバルがあります。
（同一のアラート通知の場合、同一グループにも所属することになるので、２つのintervalが経過しないと通知されません。repeat_intervalの時間は、group_intervalよりも長く設定しないと意味はなさそうです。）


### Google Home Notifier
[google home notifier](https://github.com/noelportugal/google-home-notifier)はnode.jsのライブラリです。
そのままコンポーンネントの名称に使用させてもらいました。
app.jsというソースコードを実行するようdockerfileを書きます。
node.jsは初めてだったので環境の用意も一苦労でした。
とりあえず動かすことには成功。

```:dockerfile_notifier
FROM node

RUN apt-get update
RUN apt-get install -y avahi-daemon avahi-discover libnss-mdns libavahi-compat-libdnssd-dev
RUN cat /etc/avahi/avahi-daemon.conf | grep -v rlimit-nproc > temp.conf
RUN mv -f temp.conf /etc/avahi/avahi-daemon.conf
RUN git clone https://github.com/noelportugal/google-home-notifier
RUN cd google-home-notifier && npm install

WORKDIR google-home-notifier

CMD service dbus start && service avahi-daemon start && node app.js
```
app.jsはexample.jsをほとんど流用しました。
追加したのは　
 - JSON受け取り
 - summaryをtextに代入

これくらいです。

```javascript:app.js

var express = require('express');
var googlehome = require('./google-home-notifier');
var ngrok = require('ngrok');
var bodyParser = require('body-parser');
var app = express();
const serverPort = 8091; // default port

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

var deviceName = 'Google Home';
var ip = 'x.x.x.x'; // Google Home IP address

var urlencodedParser = bodyParser.urlencoded({ extended: false });

app.post('/google-home-notifier', urlencodedParser, function (req, res) {

  var text = req.body.commonAnnotations.summary;

  if (!req.body) return res.sendStatus(400)
  console.log(text);

   if (req.query.ip) {
     ip = req.query.ip;
  }

  var language = 'ja'; // default language code
  if (req.query.language) {
    language;
  }

  googlehome.ip(ip, language);
  googlehome.device(deviceName,language)

  if (text){
    try {
      if (text.startsWith('http')){
        var mp3_url = text;
        googlehome.play(mp3_url, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will play sound from url: ' + mp3_url + '\n');
        });
      } else {
        googlehome.notify(text, function(notifyRes) {
          console.log(notifyRes);
          res.send(deviceName + ' will say: ' + text + '\n');
        });
      }
    } catch(err) {
      console.log(err);
      res.sendStatus(500);
      res.send(err);
    }
  }else{
    res.send('Please GET "text=Hello Google Home"');
  }
})


app.listen(serverPort, function () {
  ngrok.connect(serverPort, function (err, url) {
    console.log('Start Notifier');
  });
})
```


### docker-compose
作成したdockerfileや設定ファイルなどを用いて、docker-composeで立ち上げます。

```:ディレクトリ構造
.
├── docker-compose.yml
├── dockerfile
│   ├── dockerfile_alert
│   ├── dockerfile_notifier
│   └── dockerfile_snmp
├── google-home-notifier
│   └── app.js
└── yml
    ├── alert.rules.yml
    ├── alertmanager.yml
    ├── prometheus.yml
    └── snmp.yml
```

```yml:docker-compose.yml
version: '2'
services:
  prometheus:
    image: prom/prometheus
    ports:
      - 9090:9090
    links:
      - snmp_exporter
      - alert_manager
    volumes:
      - ./yml/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./yml/alert.rules.yml:/etc/prometheus/alert.rules.yml
  snmp_exporter:
    build:
      context: .
      dockerfile: "./dockerfile/dockerfile_snmp"
    ports:
      - 9116:9116
    volumes:
      - ./yml/snmp.yml:/snmp_exporter-0.8.0.linux-amd64/snmp.yml
  alert_manager:
    build:
      context: .
      dockerfile: "./dockerfile/dockerfile_alert"
    links:
      - notifier
    volumes:
      - ./yml/alertmanager.yml:/alertmanager-0.12.0.linux-amd64/alertmanager.yml
  notifier:
    build:
      context: .
      dockerfile: "./dockerfile/dockerfile_notifier"
    volumes:
      - ./google-home-notifier/app.js:/google-home-notifier/app.js
```

ｶﾁｬｶﾁｬｶﾁｬ…ｯﾀｰﾝ!!
`docker-compose up -d`
コンテナが立ち上がったらlocalhost:9090にアクセスして、Prometheusの設定やアラートを確認してみるといいと思います。
ちなみにPrometheusの画面はこんな感じです。
![スクリーンショット 2018-01-01 0.56.23.png](https://qiita-image-store.s3.amazonaws.com/0/223892/f129dc28-c1b5-fbd3-447a-4c0e75bd559a.png)



## アラートを喋らせる
それではインターフェースのup/downのテストをしてみます。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Google Home MiniにインターフェースのUp/Downを喋らせる(発話) <a href="https://t.co/BUlJVHhPlg">pic.twitter.com/BUlJVHhPlg</a></p>&mdash; ecodrive (@ecodrive_t) <a href="https://twitter.com/ecodrive_t/status/947512562509234181?ref_src=twsrc%5Etfw">2017年12月31日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



ちゃんと喋ってくれました。いい感じです。


指定したmp3も流せるようだったので、こちらの記事を参考に、mp3を返すwebサーバを作成しました。

- [帰宅したらGoogleHomeから好きな音声で「おかえり」って言ってもらいます](https://qiita.com/taaaaaaak22/items/abfb9f2a672689c20505)

アラートのsummaryにmp3のアドレスを記述しておくと、こんなこともできます。
音源は効果音ラボ様より拝借しました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Google Home MiniにインターフェースのUp/Downを喋らせる(音声データ) <a href="https://t.co/JztYHj1KEl">pic.twitter.com/JztYHj1KEl</a></p>&mdash; ecodrive (@ecodrive_t) <a href="https://twitter.com/ecodrive_t/status/947512876989784066?ref_src=twsrc%5Etfw">2017年12月31日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

(ﾉﾟοﾟ)ﾉ ｵｵｵｵｫｫｫｫｫｫ-


## 終わりに
Google Home MiniにPrometheusのアラートを喋らせてみました。
思いの外、楽しかったです。
Google Homeは話しかけるだけでなく、喋らせることでも有効に活用できそうです。