DjangoでNetwork機器のダッシュボードを作ろう！

[NetOpsCoding Advent Clender](http://qiita.com/advent-calendar/2016/netopscoding) 7日目の記事になります。
本記事ではDjangoを用いて、ネットワーク機器の情報を取得し、
Webアプリケーションとして、状態をまとめて確認できるダッシュボードの作成方法について共有します。
(情報取得をする対象として、主にスイッチではなくルータを対象としています)
<img width="687" alt="スクリーンショット 2016-12-07 5.09.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/76968/aac2816f-4471-c0be-b4eb-9d8e80063b4c.png">


**今回作成するダッシュボードは下記gitにアップロードしてあります。Qiitaでは細かなところまでは解説してないので、より詳細は現物を御覧ください**
https://github.com/Mabuchin/netboard

# はじめに
ネットワーク機器の状態を取得するツールは、古来より今に至るまで、様々なツールが開発されています。[Cacti](http://www.cacti.net/),[MRTG](http://www.mrtg.jp/doc/),[Zabbix](http://www.zabbix.com/jp/),[Munin](http://munin-monitoring.org/)等々...
勿論、これらのツールはうまく使えばネットワーク状態を確認するために非常に有益なツールであり、
実際様々な運用現場でのスタンダードとなっているはずです。
しかしながら、実際の運用現場で使う場合に、もう1つ機能がほしい、これも見れたらいいのにな・・・
といったような現象は多いと思います。
ネットワークの運用現場といっても一括りではなく、各運用現場によって、「ひと目で確認したい」と思うものはそれぞれ違うはずです。特にトラブルシュートでの状態確認をする際に、まず何を見るかは、何を監視しているかによって変化しますよね。

今回は、上記のような**監視ツールを定常的に使いつつ**も、トラブル時、及び作業中に欲しい情報だけを**Webの1ページ**でまとめて見れるようなDashboardを作ってみます。
# 取得情報と取得方法
今回は、**[BGP](https://www.nic.ad.jp/ja/newsletter/No35/0800.html)や[ASの運用](https://www.nic.ad.jp/ja/basics/terms/as.html)**をしているネットワーク運用者を想定し、取得する情報を決定します。
自分で作って見る場合は各組織の要件に合わせて作っていけば良いですが、今回は私が最も想定しやすい上記で考えます。
今回確認するものは、下記の通りです。

- 各ルータ自体の死活(Ping監視)
- Interfaceの状態
- BGPのNeighbor状態
- 機器log情報
- 特定ポートのリアルタイムなトラフィック流量
- CPU/Memoryのリアルタイムな遷移
- 状況に応じた、任意のコマンドでの確認

今回は上記を見せるようなダッシュボードを作りましょう。

# インストール
今回利用する外部ライブラリは下記が主なものです。
Pythonはpipから、JavaScrip等フロントの表示モジュール各種はWebから入手します。

- Python(2.7.9)
  - Django(1.10.4) 
  - Exscript(2.1.503)
  - tornado(4.4.2)
- Javascript
  - jQuery
  - highchart 
  - quicksearch
- Bootstrap3

PythonのモジュールはGithubに`require.txt`を上げているので、下記のようにして一括ダウンロードも可能も可能です。

```
cd ./netboard
pip install -r requirements.txt
```

１から利用する場合はDjangoのプロジェクトセットアップが必要ですので、
下記のように、Djangoプロジェクトの作成、アプリケーション作成の呪文を流れるように唱えて下さい
※cloneしたリポジトリを使う場合も、`startproject`と`startapp`以外は実行して下さい

```python
% django-admin.py startproject netopboard
% cd netopboard
% python manage.py createsuperuser
(Snip)
% python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
%python manage.py startapp device_mon
```

完了したら、ちゃんとDjangoが動いてるかチェックしましょう

```
% python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
December 05, 2016 - 08:16:56
Django version 1.10.4, using settings 'netopboard.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```


# ルータ情報を管理modelの作成
機器から情報を取得する際には、そのルータへアクセスするための認証情報が必要で、
大抵の場合は、対象のIPアドレス,SNMP Community,ログインパスワード等が必要です。
今回はDjangoのModelでそれらの情報を定義し、簡単にデータベースを作成できるようにしてみます。
DjangoのDBモデルの定義は、プロジェクト単位ではなく、その内部のアプリ事での管理となる為、作成したアプリ`device_mon`配下での定義となります。
今回は、機器のOSによって挙動を変えなければならないため、機器のOS情報も含めます。

 - ホスト名
 - IPアドレス
 - SNMP Community
 - ログインユーザ名
 - ログインパスワード
 - ルータの状態(アクセスできる状態か)

これらをDjangoのモデルとして定義すると下記のようになります。

```python:device_mon/models.py
class Device(models.Model):
    hostname = models.CharField('Hostname',max_length=255,unique = True,)
    management_ip = models.GenericIPAddressField('Management IP address',null = True,blank = True,)
    snmp_community = models.CharField('SNMP community',max_length = 255,null = True,blank = True,)
    os = models.CharField('OS',max_length = 255,validators = [AsciiValidator],)
    user = models.CharField('User Name',max_length = 255,null = True,blank = True,)
    password = models.CharField('User Password',max_length = 255,null = True,blank = True,)
    enable_password =  models.CharField('Enable Password',max_length = 255,null = True,blank = True,)
    status = models.PositiveIntegerField('Status',default = 0)

    class Meta:
        verbose_name = 'Device'
        verbose_name_plural = 'Devices'

    def __unicode__(self):
        return self.hostname
```
本来であればValidator等で、入力ルールを決めたほうが良いのですが、今回はその点については省略します。
モデルを定義した後は、実際にDBを作成する必要があるので、下記のようにしてDBを構築しましょう。

```python
% python manage.py makemigrations device_mon
Migrations for 'device_mon':
  device_mon/migrations/0001_initial.py:
    - Create model Device
```

ここまできたら、土台は完成です:v:

**これ以降の解説については、全てを解説しているわけではありません。
   要点だけ掻い摘んでいるため、省略されている部分があります。詳細はGithubのコードを参照して下さい**

# ホーム画面の作成
まずはホームの画面を作りましょう。
ダッシュボードのホームとなるようなページを作ると、こんなページができます！
<img width="674" alt="スクリーンショット 2016-12-06 5.15.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/76968/2d60a966-70ca-e18e-646f-c68401925fad.png">


### Viewsとtemplate
まずは、ネットワーク機器一覧を表示し、死活状態の確認と詳細ページ遷移のためのトップページを作ります。
ここでルータの情報をリスト形式で表示したいので、`Views.py`から下記のようにhtmlを描写させました。
htmlはDjango内のテンプレートエンジンを利用しての記述です。

```python:views.py
from django.shortcuts import render
from device_mon.models import Device

def device_list(request):
    return render(request, 'device_mon/index.html', dict(devices=Device.objects.all().order_by('hostname')))
```

viewsではDeviceのオブジェクトを全て取得し、templateに引き渡しています。
templateでは、それを受け取ってその情報を元にhtmlの描写を行います。
templateでは、受け取った情報を表示させる他ifを使っての分岐等が可能です。
Deviceのオブジェクトを受取り、その数だけtableの列を、内容を反映しながら描写する単純なテンプレートです。
死活監視の部分はNGであれば詳細情報の画面へ遷移させないよう、`if`によってボタンを変化させてます。

```html:template/index.html
    <table class="table table-bordered table-condensed sortable table-hover" data-sort-key="0" id="bgp_statuses">
      <thead>
      <tr>
        <th>Hostname</th>
        <th>Address</th>
        <th>Access</th>
        <th>Detail</th>
      </tr>
      <tbody>
            {% for device in devices %} 
            <tr>
                <div class="form-group">
                    <td><strong>{{ device.hostname }}</strong></td>
                    <td class="target-data">{{ device.management_ip }}</td>
                    {% if device.status == 1 %}
                    <td><button type="submit" class="btn btn-success btn-xs btn-block " disabled="disabled">OK</button></td>
                    <form class= "form-inline" action="{% url 'device_mon:result' %}" method="post" role="form">
                        <input type="hidden" name="routerinfo_id" value="{{ device.id }}">
                        {% csrf_token %}
                        <td><button type="submit" class="btn btn-primary btn-xs btn-block">Check</button></td>
                    </form>
                    {% else %}
                    <td><button type="submit" class="btn btn-danger btn-xs btn-block" disabled="disabled">NG</button></td>
                    <td><button type="submit" class="btn btn-default btn-xs btn-block" disabled="disabled">Check</button></td>
                    {% endif %}
                </div>
            </tr>
            {% endfor %}
      </tbody>
      </table>
```

#### tips : management commandsを利用してDBに簡単アクセス
トップページではルータ一覧のついでに、各ルータの死活監視を行っています。
これは裏でpingを定期的に実施して、その結果を格納しているだけなのですが、その際、完全に外部のプログラムでpingを実施してDBに格納するという手順では、直接DBを編集するような操作が必要になってしまいます。
ですが、`Views.py`等を見てもらえれば分かるように、Djangoのアプリケーション内部からであれば`models`の呼び出しによって、簡単にオブジェクトの操作を行うことが出来ます。

Webからではなく、外部からのオブジェクト変更や参照が必要な場合には、Djangoの[custom management commands](https://docs.djangoproject.com/ja/1.10/howto/custom-management-commands/)を利用する、Djangoアプリの`manage.py`の追加コマンドとしてプログラムを実行できます。
今回であれば、`devices_alive.py`ようなプログラムを`/device_mon/management/commands/`配下してから、`python manage.py devices_alive`と実行することで、死活監視の結果をDjangoのModel操作によってデータベースに反映することができます

```python:devices_alive.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand
from device_mon.models import Device
import pyping


class Command(BaseCommand):
    def handle(self, *args, **options):

        devices = Device.objects.all()

        for device in devices:
            response = pyping.ping(device.management_ip, count=1, timeout=1000)
            if response.ret_code == 0:
                device.status = 1
            else:
                device.status = 0

            device.save()
```

# BGP/Interface状態のパース
SNMPではなく、ルータにログインして直接取得したい情報については、ご存知expectで取得します。
勿論、この部分についてはnetconfや各種ネットワーク機器のAPIでの置き換えも可能です。
が、netconf等が対応していない、または様々な事情によって利用できない機器に対しても利用したいため、今回は悲しみのexpectを実施します。
定期的に取りに行くわけではなく、確認が必要な場合のみ取りに行けばいいので、Djangoのトップで、Checkボタンを押したら取りに行くような構造にしてみます。呼び出しは下記の様な流れになります。

1. HTML POST
2. urls.pyで検知し対象のViewsに動作をパス
3. Views.pyでexpectのスクリプトを呼び出し
4. 結果をViewsからhtmlへ返しWebに反映

取得にはpexpectの亜種であるExscriptを利用しています。
下記の記事が非常に参考にしました
[ソフトウェアからルータにSSH(Exscript)で設定してみる](http://qiita.com/taijijiji/items/351c48a8a77ee56f6e79)

# Web画面越しに任意のコマンドを実行
ポータルだけで把握できる情報には限界があるので、より詳細情報を得るために、ルータで確認のコマンドを打ちたい場合に便利です。こんな感じ:arrow_down:
![cmd_response.gif](https://qiita-image-store.s3.amazonaws.com/0/76968/5b6ffbac-48cf-016a-a73e-bdd86375ddc6.gif)

実行したいコマンドを、urls.pyを利用して裏のExscriptに送り込み、その応答内容をmodal部分に反映させています。
### Ajaxによる一部画面のみの再描写
コマンドによって実行するための引数が変わってくるため、今回はURLに取得したいコマンドを入れ込む形にしています。

```python:urls.py
    url(r'^cmd_response/(?P<device_id>\d+)/(?P<cmd>.+)$', views.get_cmd_response, name='cmd_response')
```
`device_id`が機器特定の為のDBのid、`device_id`が実際に打ちたいコマンド部分とし、値が戻ってきたら、
Ajaxでmodalの部分のみを再描写させ、完了した後にModalを表示させてます。

```javascript:
  var AjaxReload = function(url,target_id) {
          $.ajax({
                 url: url,
                 dataType: 'html',
                 beforeSend: function(){
                    dispLoading("Now Loading...");
                  },
                 success: function(data) {
                     $(target_id).html(data);
                 },
                 error:function() {
                     alert('Reload Error!');
                     removeLoading();
                 },
                 complete : function() { ///コマンド実行結果の戻りを待ってからmodalを表示
                     removeLoading();
                     if(target_id=="#cmd_exec_result"){
                        $('#commandModal').modal('show')
                     }
                 }
          });
  }
```

```python:device_mon/views.py

#plain cmd exec
def get_cmd_response(request,device_id,cmd):
    if cmd == 'None':
        cmd_result = 'None'
    else:
        cmd = cmd.replace('_',' ') # replace : show_bgp_su -> show bgp su
        device = Device.objects.get( pk = device_id )
        cmd_result = get_cmd(device,cmd)
    context = {
            'cmd_response'   : cmd_result,
            }
    return render(request,'device_mon/cmd_modal.html',context)
```

# Traffic,CPU,Memoryのリアルタイム取得
WebSocketを用いて、裏でSNMPを取得し、その結果を返すような形を取っています。
ココに関しては、去年書いたNetOpsCoding Advent Calenderの下記を参考にして下さい。

[トラフィックをWebSocketでリアルタイムに描写してみた](http://qiita.com/Mabuchin/items/135a9edff34a3db00744)
http://qiita.com/Mabuchin/items/135a9edff34a3db00744

をごらんください。
Websocketのサーバーサイドプログラムはデーモン化しませんでしたが、こんな感じで立ち上げとけばオッケーです。

```
cd websock_server
nohup python if_traffic_wss.py &
nohup python process_wss.py &
```

補足ですが、CPUやメモリはベンダ各社のMIBを利用しなければいけないケースが多いです。
IOS-XRとJUNOSのSNMP OIDは下記を利用しています。

```python:
JUNOS_CPU_USE = '.1.3.6.1.4.1.2636.3.1.13.1.8.9.1.0.0'
JUNOS_MEM_USE = '.1.3.6.1.4.1.2636.3.1.13.1.11.9.1.0.0'
IOSXR_CPU_USE = '.1.3.6.1.4.1.9.9.109.1.1.1.1.7.2'
IOSXR_MEM_USE = '.1.3.6.1.4.1.9.9.221.1.1.1.1.18'
IOSXR_MEM_FREE = '.1.3.6.1.4.1.9.9.221.1.1.1.1.20'
```
IOS-XRはメモリの利用率は一発で取得することはできません。
利用中メモリ/(空きメモリ+使用中メモリ)
といった形で求めてから,WebSocketのClientにpushしています。

```python
mem_use = int((float(mem_use) / float(mem_use+mem_free) ) * 100)
```

# 最後に
長くなってしまいましたが、自前ダッシュボードの作り方を紹介しました。
最近はモニタリングも便利なツールが増えてきていますが、自前の運用ポリシーと完全にマッチしたものを探すのは難しいです。定常的な監視はそれらを合わせて見ていけば良いと思います。
ですが、メンテナンス時や障害時など、すぐに確認すべき情報が一括で見れるようなツールを必要としている方は、こんな感じ運用ポリシーとマッチするポータルを作ってみるのも一つの手かなと思います。

#参考リンクまとめ
[Netboard(今回作ったダッシュボード)](https://github.com/Mabuchin/netboard/tree/master/netopboard/device_mon)
[Django入門(1)](http://qiita.com/kaki_k/items/511611cadac1d0c69c54)
[トラフィックをWebSocketでリアルタイムに描写してみた](http://qiita.com/Mabuchin/items/135a9edff34a3db00744)
[ソフトウェアからルータにSSH(Exscript)で設定してみる](http://qiita.com/taijijiji/items/351c48a8a77ee56f6e79)