"show weather" コマンド作ってみた

# はじめに
この記事はシスコの有志による Cisco Systems Japan Advent Calendar 2018 の 10 日目として投稿しています。

- 2017年版: https://qiita.com/advent-calendar/2017/cisco
- 2018年版: https://qiita.com/advent-calendar/2018/cisco

# 目次
- [データセンターとは](#データセンターとは)
- [今回作ったもの](#今回作ったもの)
- [show commandとは](#show-commandとは)
- [NX-SDK](#nx-sdk)

# データセンターとは
データセンターとは、サーバーやネットワーク機器などのIT機器を設置、運用する施設・建物の総称です。
(参考) [用語集｜データセンター](https://www.idcf.jp/words/date-center.html) (IDC フロンティア様ホームページから)

実際の雰囲気は、Googleさんのデータセンターの紹介ビデオをご覧ください。
[![Inside a Google data center (Youtube)](http://img.youtube.com/vi/XZmGGAbHqa0/0.jpg)](http://www.youtube.com/watch?v=XZmGGAbHqa0)

ご覧いただいてわかる通り、下記の様な特徴が言えます。

* 窓がない
* 厳重なセキュリティで守られている

こんな現場で働いている人には、こんな悩みがあります。

> 休憩で外に出たら、雨が降っていた。。。
> 傘を取りに戻るには、たくさんのセキュリティゲートを通過する必要がある。。
> 外に出る前に天気が分かればいいのに。。。。

外に出る直前、手元にあるのは、ネットワーク機器と、操作する端末のみ。。
このネットワーク機器で天気予報を参照しよう！！
ということで、実装してみました。

# 今回作ったもの
データセンター向けスイッチである [Cisco Nexus 9000 シリーズ](https://www.cisco.com/c/ja_jp/products/switches/nexus-9000-series-switches/index.html)上で、天気予報を見れる様にしました。

実際の動作の様子はこちらです。
![show_weather2.gif](https://qiita-image-store.s3.amazonaws.com/0/135003/2daec5b7-f734-65b9-6362-824f6a2be9d4.gif)

ただ表示するだけではなく、他の設定と、同様にパラメータを設定できる様にしました。

# 今回利用した機器
- [Cisco Nexus 92160YC-X](https://www.cisco.com/c/ja_jp/products/switches/nexus-92160yc-switch/index.html) (N9K-C92160YC-X)
    - version 9.2(2)

# show commandとは
Ciscoをはじめとする、様々なネットワーク機器には、状況を確認するための **show** というコマンドが実装されております。
こちらを利用して、稼働状況や実際の設定などを参照し、機器を運用いたします。

# NX-SDK
NX-SDKは、Cisco Nexus 9000 上で任意のアプリケーションを稼働させるためのSDKです。
C++で書かれたライブラリが提供されているため、これらを利用することが可能です。
また、PythonやGoなどに向けライブラリもあり、様々な言語で実装できるのもポイントです。

詳細は、昨年のCisco Advent Calendar の [こちらの記事](https://qiita.com/kkureish/items/2958b3385b91c71b7050) をご覧ください。

## プログラムを書く

実際のプログラムは、こちらです。
<details><summary>weatherPyApp.pyの本体</summary><div>

```python:weatherPyApp.py
#!/isan/bin/nxpython
# -*- coding:utf-8 -*-

################################################################
# File:   Show Weather
#
##################################################################

import signal
import time
import threading
import sys
import commands
import json

### Imports NX-OS SDK package
import nx_sdk_py

def get_location_id(location = "tokyo"):
    location_code = {
        "tokyo": "130010",
        "sapporo": "016010",
        "sendai": "040010",
        "nagoya": "230010",
        "osaka": "270000",
        "hiroshima": "340010",
        "fukuoka": "400010"
    }

    return location_code.get(location, "130010")

def get_weather(location = "tokyo", proxy = "", vrf = "default"):

    global tmsg
    command = "ip netns exec " + vrf

    base_url = "http://weather.livedoor.com/forecast/webservice/json/v1?city="
    url = base_url + get_location_id(location)

    command += " curl -s " + url

    if proxy != "":
        command += " -x " + proxy

    result = commands.getoutput(command)

    if len(result) == 0:
        return None
        
    return json.loads(result)

def weather_detail(result):
    location = result["location"]["city"].encode('utf-8')
    summary = result["forecasts"][0]['telop'].encode('utf-8')
    max_temp = result["forecasts"][0]['temperature']['max']
    if max_temp is not None:
        max_temp = float(max_temp['celsius'])

    min_temp = result["forecasts"][0]['temperature']['min']
    if min_temp is not None:
        min_temp = float(min_temp['celsius'])

    link = result["link"].encode('utf-8')

    message = "今日の%sの天気は、%s\n" % (location, summary)
    
    if max_temp is not None:
        message += "最高気温は、%.2f度\n" % max_temp
    if min_temp is not None:
        message += "最低気温は、%.2f度\n" % min_temp

    message += link + '\n'
    
    return message

def weather_summary(result):
    return result["forecasts"][0]['telop'].encode('utf-8')

class pyCmdHandler(nx_sdk_py.NxCmdHandler):
    location = "tokyo"
    vrf = "default"
    proxy = ""
    
    def postCliCb(self,clicmd):
        global cliP, location
        
        if "show_weather" in clicmd.getCmdName():
            weather_result = get_weather(self.location, self.proxy, self.vrf)

            if weather_result is None:
                clicmd.printConsole("Could Not access weather API\n")
                return False
            
            message = ""

            if "detail" in clicmd.getCmdLineStr():
                message = weather_detail(weather_result)
            else:
                message = weather_summary(weather_result) + "\n"
                
            clicmd.printConsole(message)
        elif "config_weather_location" in clicmd.getCmdName():
            if "no" in clicmd.getCmdLineStr():
                self.location = "tokyo";
            else:
                self.location = nx_sdk_py.void_to_string(clicmd.getParamValue("<location>"))
            
        elif "config_weather_vrf" in clicmd.getCmdName():
            if "no" in clicmd.getCmdLineStr():
                self.vrf = "default";
            else:
                temp_vrf = nx_sdk_py.void_to_string(clicmd.getParamValue("<vrf>"))
                if temp_vrf == "all":
                    clicmd.printConsole("Can not configure VRF to all")
                    tmsg.event("Can not configure VRF to all")
                    return False
                self.vrf = temp_vrf
                
        elif "config_weather_proxy" in clicmd.getCmdName():
            if "no" in clicmd.getCmdLineStr():
                self.proxy = "";
            else:
                self.proxy = nx_sdk_py.void_to_string(clicmd.getParamValue("<proxy>"))
                
        return True

def sdkThread(name,val):
    global cliP, sdk, event_hdlr, tmsg

    sdk = nx_sdk_py.NxSdk.getSdkInst(len(sys.argv), sys.argv)
    if not sdk:
        return
    
    sdk.setAppDesc('Weather App for Nexus')
    
    tmsg = sdk.getTracer()
    
    tmsg.event("[%s] Started service" % sdk.getAppName())
    
    cliP = sdk.getCliParser()
    
    nxcmd = cliP.newShowCmd("show_weather", "[detail]")
    nxcmd.updateKeyword("detail", "For Detail Weather Information")

    nxcmd1 = cliP.newConfigCmd("config_weather_location", "location <location>")
    nxcmd1.updateKeyword("location", "Location to check weather")

    str_attr = nx_sdk_py.cli_param_type_string_attr()
    str_attr.length = 25;
    str_attr.regex_pattern = "^[a-zA-Z]+$";
    nxcmd1.updateParam("<location>", "Location Name", nx_sdk_py.P_STRING, str_attr, len(str_attr))

    nxcmd2 = cliP.newConfigCmd("config_weather_vrf", "use-vrf <vrf>")
    nxcmd2.updateKeyword("use-vrf", "Configures Weather to use the selected VRF to fetch weather information")
    nxcmd2.updateParam("<vrf>", "VRF name", nx_sdk_py.P_VRF)

    nxcmd3 = cliP.newConfigCmd("config_weather_proxy", "proxy <proxy>")
    nxcmd3.updateKeyword("proxy", "Configures Weather to use the proxy server Ex) proxy.esl.cisco.com:8080 ")

    str_attr_url = nx_sdk_py.cli_param_type_string_attr()
    str_attr_url.length = 128;
    str_attr_url.regex_pattern = "^([^/ :]+):?([0-9]*)$";
    nxcmd3.updateParam("<proxy>", "Proxy Server", nx_sdk_py.P_STRING, str_attr_url, len(str_attr_url))

    mycmd = pyCmdHandler()
    cliP.setCmdHandler(mycmd)
    
    cliP.addToParseTree()
    
    sdk.startEventLoop()
    
    tmsg.event("Service Quitting...!")
    
    nx_sdk_py.NxSdk.__swig_destroy__(sdk)
    
cliP = 0
sdk  = 0
tmsg = 0

### create a new sdkThread to setup SDK service and handle events.
sdk_thread = threading.Thread(target=sdkThread, args=("sdkThread",0))
sdk_thread.start()

sdk_thread.join()
```

</div></details>

長いですね。。。。
ポイントを何点か。

- [新しいコマンドの定義](#新しいコマンドの定義)
- [新しいコマンドの動作の定義](#新しいコマンドの動作の定義)
- [VRFを意識して、外部接続](#VRFを意識して、外部接続)

### 新しいコマンドの定義

実際にshow commandと config commandを作成しているところをピックアップしてみました。

```python:コマンドの定義
nxcmd = cliP.newShowCmd("show_weather", "[detail]")
nxcmd.updateKeyword("detail", "For Detail Weather Information")

nxcmd1 = cliP.newConfigCmd("config_weather_location", "location <location>")
nxcmd1.updateKeyword("location", "Location to check weather")

str_attr = nx_sdk_py.cli_param_type_string_attr()
str_attr.length = 25;
str_attr.regex_pattern = "^[a-zA-Z]+$";
nxcmd1.updateParam("<location>", "Location Name", nx_sdk_py.P_STRING, str_attr, len(str_attr))
```
``newShowCmd`` と ``newConfigCmd``が、新しいコマンドを定義するメソッドです。
第1引数で指定しているものが、内部で呼び出されるコマンド名です。
第2引数には、それ以降に指定するオプションを入力いたします。

``"show_weather"``の場合、``"[detail]"``とすることで、detailがある場合、ない場合の両方を同時に定義しています。
``show + このプログラム名`` というコマンドを実行した時に、このプログラムが呼び出されます。

``updateKeyword``は、コマンドを実行する際の説明を定義するメソッドです。
コマンドを途中まで入力し、"?"を入力することで、コマンドの説明が表示されますが、そちらで利用されます。

```text:commandオプション
n92160-01# show weather ?
  <CR>    
  >       Redirect it to a file
  >>      Redirect it to a file in append mode
  detail  For Detail Weather Information
  nxsdk   Auto-generated commands by NXSDK
  |       Pipe command output to filter
```

``updateParam``が実際にconfigを設定した際のパラメータになります。
``nx_sdk_py.cli_param_type_string_attr()``が、パラメータの制限事項を書きます。
正規表現を利用することで、こちらで入力する文字列の制限が可能です。

文字列以外にも、integer (数値)なども制限することができます。
詳細は、ソースコード ([include/types/nx_cli.h](https://github.com/CiscoDevNet/NX-SDK/blob/master/include/types/nx_cli.h) )をご覧ください。

### 新しいコマンドの動作の定義

上の方法で、各コマンドの定義をしました。
それでは、こちらで実際にコマンドを叩いた時の動作を定義します。

```python:コマンドの動作
def postCliCb(self,clicmd):
    global cliP, location

    if "show_weather" in clicmd.getCmdName():
        weather_result = get_weather(self.location, self.proxy, self.vrf)
        ## 省略 ##
        message = weather_summary(weather_result) + "\n"
        clicmd.printConsole(message)
    elif "config_weather_location" in clicmd.getCmdName():
        if "no" in clicmd.getCmdLineStr():
            self.location = "tokyo";
        else:
            self.location = nx_sdk_py.void_to_string(clicmd.getParamValue("<location>"))
```

このプログラム内で定義されたコマンド (show コマンドも config コマンドも)は、どちらも同一のメソッド ``postCliCb``で呼び出されます。
そのため、``clicmd.getCmdName()``を利用し、実際に叩かれたコマンドを取得しています。

それに応じて条件を分岐させて、処理をしています。
画面に出力するには、``clicmd.printConsole``が利用可能です。
また、設定したパラメータの内容を取得するのには、``nx_sdk_py.void_to_string``を利用します。

global変数を利用して、パラメータを保持することも可能ですが、
他のメソッドやクラスから変数を参照することがないので、今回は、インスタンス変数として保持しています。

また、config時に"no"が含まれていたら、デフォルト値 (locationの場合は、"tokyo")を設定する箇所も自分で作ります。

### VRFを意識して、外部接続

Nexus上には、複数のVRFができ、それぞれ別のRoutingテーブルを持ちます。
インターネットアクセスする際には、こちらを意識する必要があるが、
なかなか良い方法が見つからず、python上から bash上のcurlを実行することで任意のwebページを取得しています。

天気予報は、[Weather Hacks](http://weather.livedoor.com/weather_hacks/webservice)を利用しています。

## プログラムを動かす

自作したプログラムを実行するには、下記手順が必要となります。

1. [必要な機能を有効にする](#1-必要な機能を有効にする)
1. [プログラムをNexus上にコピーする](#2-プログラムをnexus上にコピーする)
1. [Bashにログインする](3-bashにログインする)
1. [Bash上でプログラムを実行する](#4-bash上でプログラムを実行する)

### 1. 必要な機能を有効にする
NX-SDKを利用するには、bashとNX-SDKを有効にする必要があります。

```text
n92160-01# conf t
Enter configuration commands, one per line. End with CNTL/Z.  
n92160-01(config)# feature nxsdk
n92160-01(config)# feature bash-shell
```

### 2. プログラムをNexus上にコピーする
前の章で作ったプログラムをNexusに転送します。

```shell
n92160-01# copy tftp://x.x.x.x/weatherPyApp.py bootflash://weather
```
この際にポイントになるのは、ファイル名がそのまま、コマンドになるので、
拡張子などを取り除き、**weather** だけにしてます。

### 3. Bashにログインする
Nexus上からBashにログインするには、下記コマンドを実行します。

```shell
n92160-01# run bash sudo su
```

実際にプログラムを起動させる際に、rootになる必要があるため、`sudo su`を付け加えてます。


### 4. Bash上でプログラムを実行する
NexusのBash上でプログラムを実行します。
ただし、Bashから抜け出しても、プログラムを継続させるために、``nohup`` と ``&``を利用しています。

```shell
bash-4.3# nohup /isan/bin/python /bootflash/weather &
```
こちらで自作のコマンドを動かすことができました。

## 本プログラムの使い方

### show コマンド
2種類の出力が可能です。

```shell:show&nbsp;weather
n92160-01# show weather 
曇時々晴
```

```text:show&nbsp;weather&nbsp;detail
n92160-01# show weather detail
今日の東京の天気は、晴時々曇
最高気温は、10.00度
最低気温は、5.00度
http://weather.livedoor.com/area/forecast/130010
```

detailをつけると、場所、気温(取得できれば)、ソースのURLを追加して表示します。

### config コマンド
3つの項目を設定可能にしました。

```text:config&nbsp;weather
n92160-01(config)# weather ?
  location  Location to check weather
  proxy     Configures Weather to use the proxy server Ex)
            proxy.esl.cisco.com:8080  
  use-vrf   Configures Weather to use the selected VRF to fetch weather
            information 
```

天気予報を表示する場所を``location``
情報を取得するために利用するweb proxy serverを``proxy``
情報を取得するVRFを指定する``use-vrf``を
定義しています。

``location``は、シスコシステムズ合同会社のオフィスがある都市を指定可能です。
[シスコの日本のオフィス 一覧](https://www.cisco.com/c/ja_jp/about/localoffices.html)

# さいごに
Cisco Nexus 9000シリーズにある、自作のコマンドを作成できるNX-SDKを紹介いたしました。
他にも様々なAPI/SDKや実行環境を持っているNexusを活用して、日々の運用を楽しくしてください！

## 参考
[GitHub NX-SDK](https://github.com/CiscoDevNet/NX-SDK)
[Cisco Nexus 9000 Series NX-OS Programmability Guide, Release 9.x](https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus9000/sw/9-x/programmability/guide/b_Cisco_Nexus_9000_Series_NX-OS_Programmability_Guide_9x.html)
[DEVNET Open NX-OS](https://developer.cisco.com/docs/nx-os/#)

本当はrpmでパッケージして配布したかったが、なぜか今回の環境ではうまく動かず。。。。

# 免責事項
本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。