4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ネットワーク機器用のVS CodeのExtensionを作りたい 第一回:コマンドの一覧取得

Last updated at Posted at 2019-08-25

プログラミング言語はオシャレに書けるのになぜネットワーク機器のコンフィグはメモ帳なんだ。
そうだ、VS Codeの拡張機能を作ろう。

#モチベーション

  • メモ帳でコンフィグ書きたくない
  • 目視のlintやsyntax highlight やめたい
  • 実機を使わずにコマンドを補完したい

#流れ
ざっとした流れはこんな感じです

  1. 構文木っぽいものをつくる
  2. Syntax Highlightをつくる
  3. Diagnosticをつくる(エラー表示)
  4. Completionをつくる(補完)

今回は項番1の__構文木__を作ります。

#1.構文木をつくる

構文解析を行うにあたり、まず、機器のコマンドの一覧が必要です。今回はそれを構文木という形で用意します。
構文木はよく知らないのですが、このケースにおいては、大雑把に言うと__入力可能なコマンドが親子関係になっているもの__(と幾つかの情報)だと勝手に思ってます。
CLIだとハテナマークで入力可能なコマンドの一覧が出るので、それを収集・探索していきましょう。

今回は水色のスイッチのconfigureモード用にコードを書きました。
netmikoとかを使うともっとキレイに書けるかもしれません。
もちろん、configureモード以外にもIFモードなど色々なモードがあります。
作りたい機器のリファレンスを参照し、必要なモードのデータを取得しましょう。

下記コードを実行すると、こんな感じでログが出力されます。なんとなく趣があります。
te.gif

test.py
from datetime import datetime

import telnetlib
import re
from pprint import pprint
import types
import typing
import json

telnet = telnetlib.Telnet("10.0.0.1")

interactions = {
    'login:': "admin\n",
    '>': "enable\n",
    '#': "conf t\n",
    '(config)#': "terminal length 0\n",
}

for key, val in interactions.items():
    telnet.read_until(key.encode("ascii"))
    telnet.write(val.encode("ascii"))

print(telnet.read_until(b"(config)#").decode("ascii"))

suffix = " ?"
current_mode = "config)#"
empty_ptn = re.compile("^\\s*(config\\)#)?$", re.MULTILINE)
# キャリッジリターン ここで終端可能なコマンド
cr_ptn = re.compile("^\\x20{2}<cr>\\x20*", re.MULTILINE)
# 普通のコマンド
normal_ptn = re.compile("^\\x20{2}(\\S+)?\\x20+([^\\r\\n]+)", re.MULTILINE)
# TODO 多分必要ない
first_line = re.compile(".*$", re.MULTILINE)

# ポート番号やLAG-IDなどの、表示内容と実際に入力する内容が違うケース
variable_patterns = [
    {
        "pattern": re.compile("(PORT|MLAG)(NO|RANGE(?:<(\\d+)-(\\d+)>)?)"),
        "dummy": "1/1"
    }, {
        "pattern": re.compile("(LAG|VLAN|LIST)?(NO|(?:RANGE)?<(\\d+)-(\\d+)>)"),
        "dummy": lambda matched: matched.group(3) if matched.group(3) is not None else "1"
    }, {
        "pattern": re.compile("(?:X:X::X:X|A\\.B\\.C\\.D)(?:/M)?"),
        "dummy": lambda matched: re.sub("[A-DMX]", "1", matched.group(0))
    }, {
        "pattern": "OFFSET",
        "dummy": "+00:00:00"
    }, {
        "pattern": "YYYYMMDD",
        "dummy": "20190101"
    },  {
        "pattern": "SECOND",
        "dummy": "0"
    }, {
        "pattern": "OID",
        "dummy": "0.1"
    }, {
        "pattern": "MASK",
        "dummy": "1"
    }, {
        "pattern": "HH:MM:SS",
        "dummy": "00:00:00"
    }
]

result_txt = ""

f = open('telnetlog_{}.txt'.format(datetime.now().strftime("%m_%d_%H_%M")), 'a')


# 再帰的にコマンドを打つ
def recursive_survey(command="", index=0, entire=""):
    # 深さ制限
    if index > 20:
        print("index is outbound")
        return
    terminal_tree = {}
    # (command)#から右 使わないので捨てる
    eager = telnet.read_very_eager().decode("ascii")
    if eager:
        f.write(eager)
    # コマンドとクエスチョンマークを送信
    telnet.write((command + suffix).encode("ascii"))
    # (command)#まで読み込む
    # 送信したコマンド+クエスチョンマーク→実行可能なコマンド→(command)#の順番になるので
    f.write(str(telnet.read_until("?\r\n".encode("ascii"), timeout=10), "ascii"))
    raw_result = str(telnet.read_until(current_mode.encode("ascii"), timeout=10), "ascii")
    f.write(raw_result)

    # 現在地点から使用可能なコマンド
    branches = []
    for line in raw_result.split("\n"):
        if empty_ptn.search(line) is not None or current_mode in line:
            continue
        elif "Unrecognized" in line:
            print(line)
        elif cr_ptn.search(line) is not None:
            terminal_tree["CR"] = True
        elif normal_ptn.search(line) is not None:
            arg = normal_ptn.search(line)
            matched = arg.group(1)
            description = arg.group(2)

            # たまに説明が2行に渡るコマンドがあるので それ用
            if matched is None:
                print("continuation: " + arg.group(0))
                continue

            # sendは送信するコマンド
            branches.append({
                "view": matched,
                "send": matched,
            })
            for pattern in variable_patterns:
                sub_matched = None
                if type(pattern["pattern"]) is str and pattern["pattern"] == matched:
                    sub_matched = matched
                elif isinstance(pattern["pattern"], typing.Pattern):
                    sub_matched = pattern["pattern"].search(matched)

                if sub_matched is None:
                    continue

                if isinstance(pattern["dummy"], str):
                    branches[-1]["send"] = pattern["dummy"]
                    branches[-1]["send"] = pattern["dummy"]
                elif isinstance(pattern["dummy"], types.LambdaType):
                    branches[-1]["send"] = pattern["dummy"](sub_matched)
                break
        else:
            print("Not Found : " + line)

    # 現在地点から実行可能なコマンドを探査していく
    for branch in branches:
        branch_send_command = branch["send"]
        # vbモードは探査しきれないので
        # LINEは無限になってしまうので(スペースを許容する文字列 descriptionなど)
        if branch_send_command == "vb" or branch_send_command == "LINE" and command == "LINE":
            continue
        print("\t" * index + "-> " + branch_send_command)
        terminal_tree[branch["view"]] = recursive_survey(branch["send"], index + 1, entire + " " + branch["send"])
        print("\t" * index + "<- " + branch_send_command)
        # 打ったコマンドの文字数とスペースの数だけ文字消去(制御文字)を送信
        telnet.write(telnetlib.NAOL * (len(branch_send_command) + 1))
    return terminal_tree


result_tree = recursive_survey()
pprint(result_tree)
f.close()

f = open('commands_{}.json'.format(datetime.now().strftime("%m_%d_%H_%M")), 'a')
f.write(json.dumps(result_tree))
f.close()



実行結果はこんな感じです。
中古で買った機器とはいえ、権利関係上載せづらいので、イメージです。
実際には、configモードのみで7,500行程度、容量は250kB程度。
全モード合計で1.3MB程度でした。

実行結果.py
#権利上載せられない気がするのでイメージです
{'interface':{'port':...},{'vlan':...}}

#2.モード遷移を補足する
ネットワーク機器には、特権モード、設定モードなど様々なモードがあります。
それを移動できるように、書き出されたJSONの
enable conf t exit interface fastethernet 1などにモード遷移情報を追加しましょう。
そういったコマンドの一覧は、リファレンスに遷移図とともに載っていたりしますので
各自で対象機器のリファレンスを読んでみてください。

今回は、対象コマンドのキーにintoを追加し、その値として遷移先モードの名前を書くようにしてみました。

"enable": {
  "into": "privilege",
  ...
}

#参考にさせていただいたページ
設定ファイルエディターをつくる方へ - LGTM を参考にさせていただきました。

#つづく
今日の日曜大工はこれでおしまいです。
また進捗があったら続きを書きます。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?