Creating a NOS Plugin
NOS プラグインを作成し、サーバーを起動する前に SIMNOS インスタンスに登録できます。
NOS プラグインを作成する方法はいくつかあります:
上記の方法のいずれかが他より優れているというわけではなく、それぞれに適したユースケースがあります。ただし、作成がシンプルで柔軟性が低いものから、より複雑で柔軟性が高いものの順にリストされています。
NOS プラグインでは以下の属性を定義できます:
name- インベントリで使用するプラグインの参照名initial_prompt- 表示されるシェルプロンプトの定義または変更に使用enable_prompt-enableモードへの移行に使用(任意)config_prompt-configモードへの移行に使用(任意)commands- この NOS プラグインが出力を返すことができるコマンドの辞書
初期 NOS シェルプロンプト¶
初期 NOS シェルプロンプトは、シェル起動時にユーザーに表示されるインジケーターです。
中括弧 {} 内で定義されている場合、base_prompt フォーマッターを使用してインベントリからホスト名を参照できます。
例えば、初期プロンプトが {base_prompt}> に設定されている場合、フォーマットメソッドを適用した後の最終プロンプトは、インベントリのホスト R1 に対して R1> となります。
NOS コマンド¶
コマンドは、コマンド文字列をキーとし、出力やヘルプ、正しく呼び出すために必要なプロンプトなどのコマンドの詳細を含む別の辞書を値とする辞書です。
Python コマンド辞書のサンプル内容:
commands = {
"enable": {
"output": None, # (6)
"new_prompt": "{base_prompt}#", # (2)
"help": "enter exec prompt", # (5)
"prompt": "{base_prompt}>", # (10)
},
"show clock": {
"output": MyDevice.make_show_clock, # (9)
"help": "Display the system clock",
"prompt": ["{base_prompt}>", "{base_prompt}#"], # (3)
},
"show running-config": {
"output": """ # (4)
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname {base_prompt} # (12)
!
boot-start-marker
boot-end-marker
""",
"help": "Current operating configuration",
"prompt": "{base_prompt}#",
},
"show version": {
"output": """
Version: 0.1.0
{base_prompt} uptime is 1 day, 17 hours, 32 minutes
Uptime for this control processor is 1 day, 17 hours, 33 minutes
Configuration register is 0x2102
""",
"help": "System hardware and software status",
"prompt": "{base_prompt}#",
},
"_default_": { # (11)
"output": "% Invalid input detected at '^' marker.",
"help": "Output to print for unknown commands",
},
"terminal width 511": {
"output": "", # (8)
"help": "Set terminal width to 511"
},
"terminal length 0": {
"output": "",
"help": "Set terminal length to 0"
},
"exit": {"exit": True, "help": "Exit commands shell"} # (7)
}
- コマンド出力を生成するカスタム関数
- コマンド出力が返された後に表示する新しいプロンプト
- このコマンドが有効な現在のプロンプトのリスト(コマンドのスコープ)
- 複数行のコマンド出力
- シェルで
?またはhelpが入力された場合に表示されるヘルプメッセージ - コマンド出力として
Noneを返すとレスポンスは生成されない exit: trueを設定するとシェルが閉じる- 空の出力を返すと改行文字のみを含むレスポンスが生成される
- 出力は関数などの呼び出し可能なオブジェクトを参照でき、シェルプラグインによって実行されてレスポンス内容が生成される
- このコマンドが有効な唯一のプロンプト
- 未定義コマンドに使用されるデフォルトのレスポンス内容
- 返される static な文字列出力には
base_promptフォーマッターを含めることができる。callable の出力は handler 自身が format する (callable 契約 参照)
コマンド辞書がサポートする属性:
| Attribute | Emoji | Description |
|---|---|---|
output |
レスポンスで返すコマンド出力 | |
help |
コマンドのヘルプメッセージ内容 | |
prompt |
:simple-powershell: | このコマンドが有効なインジケーターまたはインジケーターのリスト |
new_prompt |
コマンド出力が返された後に表示する新しいプロンプト | |
alias |
呼び出し可能な関数としてのコマンド出力 | |
exit |
true の場合、シェルセッションを終了する |
コマンド辞書の output 属性の値は以下の型が使用できます:
string- レスポンスで返す1行以上の文字列。base_promptフォーマッターを含めることができます。None- レスポンスは返されませんcallable- 呼び出し可能なオブジェクトで、シェルプラグインによって実行されてレスポンス内容が生成されます。その出力はシェル側では format されません (callable 契約 参照)
exit 属性:
true- シェルセッションを終了します
prompt と new_prompt 属性に関する補足事項。
prompt はこのコマンドが現在のプロンプトのコンテキストで有効かどうかを示すフィルターとして機能します。現在のプロンプトの値がコマンドのプロンプトと等しくない場合、レスポンス出力は _default_ コマンドの出力値(通常はエラーメッセージ)から取得されます。
new_prompt は単純に、コマンド出力がユーザーに返された後、現在のプロンプト値を new_prompt の値に設定すべきであることを示します。
Create a NOS plugin from a YAML file¶
以下のサンプル内容で path/to/my_nos.yaml に YAML ファイルを作成します:
name: MySimNOSPlugin
initial_prompt: "{base_prompt}>"
commands:
enable:
output: null
new_prompt: "{base_prompt}#"
help: enter exec prompt
prompt: "{base_prompt}>"
show clock:
output: "*21:01:33.000 AET 01 01 01 2022"
help: "Display the system clock"
prompt: ["{base_prompt}#"]
show running-config:
output: |
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname {base_prompt}
!
boot-start-marker
boot-end-marker
help: "Current operating configuration"
prompt: "{base_prompt}#"
show version:
output: |
Version: 0.1.0
{base_prompt} uptime is 1 day, 17 hours, 32 minutes
Uptime for this control processor is 1 day, 17 hours, 33 minutes
Configuration register is 0x2102
help: System hardware and software status
prompt: "{base_prompt}#"
_default_:
output: "% Invalid input detected at '^' marker."
help: "Output to print for unknown commands"
terminal width 511: {"output": "", "help": "Set terminal width to 511"}
terminal length 0: {"output": "", "help": "Set terminal length to 0"}
この YAML ファイルを使用して、以下のように NOS プラグインを登録できます:
hosts:
R1:
username: user
password: user
port: 6000
nos:
plugin: path/to/my_nos.yaml
素早くテストするには、ターミナルで以下のコマンドを実行してください:
simnos -i path/to/inventory.yaml
Create a NOS plugin from a Python file¶
Python モジュールから作成された NOS プラグインは、インタラクティブ性を可能にする SIMNOS の主要な強みの一つです。コマンドの考え方は、出力が事前定義された出力ではなく、コマンドの出力を返す関数を定義できることです。これにより、コマンドの出力を動的にでき、時間、日付、ホストなどに応じて変化させることができます。Python NOS モジュールを開発する場合は、このセクションを注意深く読む価値があります。
以下のコードはテスト中に使用する Python モジュールですが、完全に機能します(Netmiko ではオブジェクトはジェネリックです):
"""
This is a testing module
"""
import time
from simnos.plugins.nos.platforms_py._templates.base_template import BaseDevice
NAME: str = "test_module"
INITIAL_PROMPT = "{base_prompt}>"
ENABLE_PROMPT = "{base_prompt}#"
CONFIG_PROMPT = "{base_prompt}(config)#"
DEFAULT_CONFIGURATION: str = "tests/assets/test_module.yaml.j2"
# noqa: ARG002
class TestModule(BaseDevice):
"""
Class that keeps track of the state of the TestModule device.
"""
def make_show_clock(self, base_prompt, current_prompt, command):
"""Return the current time."""
return str(time.ctime())
def make_show_version(self, base_prompt, current_prompt, command):
"""Return the system version."""
return "TestModule version 1.0"
commands = {
"enable": {
"output": None,
"new_prompt": "{base_prompt}#",
"help": "enter exec prompt",
"prompt": INITIAL_PROMPT,
},
"show clock": {
"output": TestModule.make_show_clock,
"help": "show current time",
"prompt": ["{base_prompt}#", "{base_prompt}>"],
},
"show version": {
"output": TestModule.make_show_version,
"help": "show system version",
"prompt": "{base_prompt}#",
},
}
分解して説明しましょう。SIMNOS はモジュールを動的にロードできますが、モジュールには特定の構造が必要です。一方では、いくつかの定数(NAME、INITIAL_PROMPT、任意で ENABLE_PROMPT / CONFIG_PROMPT)が必要で、他方ではコマンドの辞書、最後に BaseDevice を継承するクラスが必要です。これは SIMNOS がモジュールをロードするために必須です。
まず、NAME、INITIAL_PROMPT、ENABLE_PROMPT(任意)、CONFIG_PROMPT(任意)の属性があります。これらの属性は SIMNOS が NOS プラグインを登録するために必要です。NAME はプラグインの名前、INITIAL_PROMPT は初期シェルインジケーター、ENABLE_PROMPT は enable モードのシェルインジケーター、CONFIG_PROMPT は config モードのシェルインジケーターです。
device class は自動検出されます: SIMNOS はモジュール内で定義された
BaseDevice subclass を 1 つだけ採用します。他の device class を import する
こと (helper device / 型注釈用 import) は問題ありません — ローカル定義の
subclass だけがカウントされます。同一モジュール内に 2 つ以上定義すると load 時に
loud な ValueError になります。旧 DEVICE_NAME 定数 (#241 以前) は deprecation
warning 付きで無視されます — 古い plugin からは削除してください。
Note
platform が yaml と Python module の両方を持つ場合、module は後から
ロードされ、ロード済みの同名 command (典型的には yaml 由来) を
command 単位で全置換します (help / prompt / output の deep
merge はしません)。override は debug level で log されるため、module が
どの command を上書きしたか確認できます。
次に、コマンドの辞書があります。この辞書は NOS プラグインが出力を返すことができるコマンドを含む Python 辞書です。各コマンドは "output"、"help"、"prompt" の属性を持つ辞書です。出力は文字列、None、またはレスポンス内容を生成する callable にできます。ヘルプは ? または help コマンドが入力された場合にユーザーに表示されるヘルプです。プロンプトはコマンドが有効なシェルインジケーターです。
callable 契約¶
型付きの契約は simnos.core.command_contract (CommandHandler Protocol +
CommandResult TypedDict) にあります — 型注釈に使いたい場合は import してください。
形が合っていれば普通の関数のままでも動作します。
シェルは callable output を次の形で起動します:
output(device, base_prompt=..., current_prompt=..., command=...)
device はあなたの BaseDevice subclass のインスタンスです (device class を
持たない platform では None)。commands 辞書に TestModule.make_show_clock
のような unbound method を置けばこの契約を満たします: device が self に
bind されます。
callable コマンドは以下の値を返せます:
str- 表示する出力文字列None- レスポンスなしdict(CommandResult) - 以下のオプションキーを持つ辞書:"output"- 表示する文字列"new_prompt"- 新しいプロンプト値({base_prompt}フォーマッターが使用可能)"exit"-Trueの場合、シェルセッションを終了(output は表示されない)
yaml static な output と異なるルールが 2 つあります:
- format は自分で行う。 シェルは callable output に
{base_prompt}format を 適用しません — handler はbase_promptを引数で受け取り自分で文字列を render します。device 出力に literal な brace が含まれていても escape 不要です。 (new_promptは従来通りシェル側で format されます — prompt template はシェルの 関心事です。) - raise してもよい。「起きてはならない」状態 (想定外の prompt 等) では例外を
投げて構いません: シェルは full traceback を server log に記録し、client には
固定の
% Internal error1 行を返します — traceback が wire に出ることは ありません。
最後に、BaseDevice を継承するクラスがあります。このクラスは SIMNOS がモジュールを正しくロードするために必要です。内部的には、self.configurations 属性でモジュールを初期化します。モジュールが optional な DEFAULT_CONFIGURATION 属性 (YAML/Jinja2 形式の設定ファイルのパス) を定義していれば、そのファイルが辞書として self.configurations にロードされます。未定義の場合は self.configurations は空 dict になります (バンドル plugin の中ではこの hook を実際に使っているのは HuaweiSmartAX のみです)。また、simnos/plugins/nos/platforms_py/templates/ ディレクトリ内の Jinja2 テンプレートをレンダリングできる render(self, template: str, **kwargs) -> str メソッドも含まれています。これらの属性を持つクラスにすることで、モジュールの標準化に役立ちます。同時に、個別の関数ではなくクラスにすることで、コマンド間で変数を共有したり、デバイスの状態を変更することもできます。例えば、デバイスの IP を変更するコマンドを作成した場合、クラス内のデバイスの状態を変更し、残りのコマンドがこの変更を考慮して新しい IP で文字列を返すようにできます。
もちろん、独自のコマンドとロジックで独自の Python モジュールを作成することもできます。正しい構造を持ち、正しくロードできることを確認してください。SIMNOS インベントリで指定すれば、SIMNOS がロードとコマンドの登録を行います。
上記のコードを確認するために、以前と同様にインベントリの YAML を作成できます:
hosts:
R1:
username: user
password: user
port: 6000
nos:
plugin: path/to/my_nos.py
素早くテストするには、ターミナルで以下のコマンドを実行してください:
simnos -i path/to/inventory.yaml
Create a NOS plugin from the Nos class¶
Warning
Nos クラスを直接使用して NOS プラグインを開発することはメンテナンスが複雑になるため推奨されません。代わりに Python モジュールの使用を推奨します。
SIMNOS パッケージには、NOS プラグインを作成して SIMNOS インスタンスに登録するために使用できる基底クラス Nos が付属しています。結局のところ、以前と同じことを行いましたが、自分で作成する代わりに SIMNOS に任せていたのです。
インスタンス化時に必要な属性を指定して Nos クラスを使用したカスタム NOS プラグインを定義するサンプルコード:
from simnos import SimNOS, Nos
nos = Nos(
name="MySimNOSPlugin",
initial_prompt="{base_prompt}>",
commands={
"terminal length 0": {"output": "", "help": "Set terminal length to 0", "prompt": "{base_prompt}>"},
"show clock": {"output": "MySimNOSPlugin system time is 00:00:00", "help": "Display the system clock", "prompt": "{base_prompt}>"},
},
)
inventory = {
"hosts": {
"router42": {
"port": 6005,
"nos": {"plugin": "MySimNOSPlugin"},
},
}
}
net = SimNOS(inventory)
net.register_nos_plugin(plugin=nos)
net.start()
try:
while True:
pass
except KeyboardInterrupt:
net.stop()
この例では、必要な属性を持つ Nos オブジェクトが作成され、SimNOS のインスタンスに登録されます。
さらに、Nos クラスの from_dict メソッドと from_file メソッドを使用して Nos の属性を指定できます。例えば、Python ファイルから NOS プラグインを作成するセクションと同等の結果を得るには、以下のコードを使用できます:
nos = Nos(filename="path/to/my_nos.py")
inventory = {
"hosts": {
"router42": {
"port": 6005,
"nos": {"plugin": "MySimNOSPlugin"},
},
}
}
Note
2つのコマンドが同じ名前に一致する場合、最後にロードされたコマンドが使用されます。