プログラミング言語はオシャレに書けるのになぜネットワーク機器のコンフィグはメモ帳なんだ。
そうだ、VS Codeの拡張機能を作ろう。
#モチベーション
- メモ帳でコンフィグ書きたくない
- 目視のlintやsyntax highlight やめたい
- 実機を使わずにコマンドを補完したい
#流れ
ざっとした流れはこんな感じです
- 構文木っぽいものをつくる
- Syntax Highlightをつくる
- Diagnosticをつくる(エラー表示)
- Completionをつくる(補完)
今回は項番1の__構文木__を作ります。
#1.構文木をつくる
構文解析を行うにあたり、まず、機器のコマンドの一覧が必要です。今回はそれを構文木という形で用意します。
構文木はよく知らないのですが、このケースにおいては、大雑把に言うと__入力可能なコマンドが親子関係になっているもの__(と幾つかの情報)だと勝手に思ってます。
CLIだとハテナマークで入力可能なコマンドの一覧が出るので、それを収集・探索していきましょう。
今回は水色のスイッチのconfigureモード用にコードを書きました。
netmikoとかを使うともっとキレイに書けるかもしれません。
もちろん、configureモード以外にもIFモードなど色々なモードがあります。
作りたい機器のリファレンスを参照し、必要なモードのデータを取得しましょう。
下記コードを実行すると、こんな感じでログが出力されます。なんとなく趣があります。
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程度でした。
#権利上載せられない気がするのでイメージです
{'interface':{'port':...},{'vlan':...}}
#2.モード遷移を補足する
ネットワーク機器には、特権モード、設定モードなど様々なモードがあります。
それを移動できるように、書き出されたJSONの
enable
conf t
exit
interface fastethernet 1
などにモード遷移情報を追加しましょう。
そういったコマンドの一覧は、リファレンスに遷移図とともに載っていたりしますので
各自で対象機器のリファレンスを読んでみてください。
今回は、対象コマンドのキーにintoを追加し、その値として遷移先モードの名前を書くようにしてみました。
"enable": {
"into": "privilege",
...
}
#参考にさせていただいたページ
設定ファイルエディターをつくる方へ - LGTM を参考にさせていただきました。
#つづく
今日の日曜大工はこれでおしまいです。
また進捗があったら続きを書きます。