はじめに
この記事は さくらインターネット Advent Calendar 2024 2日目の記事です。
お久しぶりです。さくらインターネットの大久保です。今回は小ネタです。
入社以来、長らくネットワークの自動制御システムを作っています。以前からネットワーク機器への設定投入や状態取得処理をPerlで書いていました。Net::Telnetモジュールには20年ほどお世話になってます。
最近はメンテナンス性の観点からGo言語で実装する機会も多いです。
当方の環境では、残念ながらtelnetでしか操作できないネットワーク機器も多数残っています。いまだに対話型インターフェイス(CLI)を自動化する必要があったりするんですね。
そこで今回は、Go言語でネットワーク機器のCLIを自動化し、簡単なルータの設定を試しましたので紹介します。
ここでは以下のパッケージを利用しています。
サンプルコード
早速ですが、サンプルコードです。操作対象の機器は Cisco Catalyst IOS-XE
の例となります。
処理のおおまかな流れは以下の通りです。
- 対象機器にtelnet接続する
- パスワードを送信し、ログイン処理を経てenableモードに遷移する
- ページャをオフにする
- configureモードに移行し、設定を投入する
ここではサンプルとしてmac address-table aging-time 86400
を入れてます - 設定を不揮発性メモリに保存する
- show running-config の出力結果を取得する
package main
import (
"fmt"
"os"
"regexp"
"time"
expect "github.com/google/goexpect"
"github.com/ziutek/telnet"
)
const (
remoteHostName = "TEST-Cat4500X-1"
remoteHostAddrPort = "192.168.XX.XX:23"
loginPW = "secret"
enablePW = "secret"
)
var (
passwordPrompt = `Password: `
userPrompt = remoteHostName + ">"
enablePrompt = remoteHostName + "#"
configPrompt = remoteHostName + `\(config.*\)#`
timeout = 10 * time.Second
)
func main() {
// telnet接続をオープン
conn, err := telnet.DialTimeout("tcp", remoteHostAddrPort, timeout)
if err != nil {
panic(err)
}
// expectを開始
resCh := make(chan error)
e, _, err := expect.SpawnGeneric(&expect.GenOptions{
In: conn,
Out: conn,
Wait: func() error {
return <-resCh
},
Close: func() error {
close(resCh)
return conn.Close()
},
Check: func() bool {
return true
},
}, timeout, expect.Tee(os.Stderr))
if err != nil {
panic(err)
}
defer e.Close()
_, err = e.ExpectBatch([]expect.Batcher{
// パスワードプロンプトを待つ
&expect.BExp{R: passwordPrompt},
// ログインパスワードを送信しuserモードプロンプトを待つ
&expect.BSnd{S: loginPW + "\n"},
&expect.BExp{R: userPrompt},
// enableモードに移行
&expect.BSnd{S: "enable\n"},
&expect.BExp{R: passwordPrompt},
&expect.BSnd{S: enablePW + "\n"},
&expect.BExp{R: enablePrompt},
// ページャをオフにする
&expect.BSnd{S: "terminal length 0\n"},
&expect.BExp{R: enablePrompt},
// configureモードに移行し、設定を投入する
&expect.BSnd{S: "configure terminal\n"},
&expect.BExp{R: configPrompt},
&expect.BSnd{S: "mac address-table aging-time 86400\n"},
&expect.BExp{R: configPrompt},
// configモードを抜ける
&expect.BSnd{S: "end\n"},
&expect.BExp{R: enablePrompt},
// 設定を保存する
&expect.BSnd{S: "write memory\n"},
&expect.BExp{R: enablePrompt},
}, timeout)
if err != nil {
panic(err)
}
// running-configを取得する
if err = e.Send("show running-config\n"); err != nil {
panic(err)
}
resp, _, err := e.Expect(regexp.MustCompile(enablePrompt), timeout)
if err != nil {
panic(err)
}
fmt.Print(resp)
}
実行例
expect.SpawnGeneric
に expect.Tee(os.Stderr)
を渡しています。これにより、ターミナルログが標準エラー出力に表示されます。
$ go run .
User Access Verification
Password:
TEST-Cat4500X-1>enable
Password:
TEST-Cat4500X-1#terminal length 0
TEST-Cat4500X-1#configure terminal
Enter configuration commands, one per line. End with CNTL/Z.
TEST-Cat4500X-1(config)#mac address-table aging-time 86400
TEST-Cat4500X-1(config)#end
TEST-Cat4500X-1#write memory
Building configuration...
Compressed configuration from 4345 bytes to 2150 bytes[OK]
TEST-Cat4500X-1#show running-config
Building configuration...
Current configuration : 4345 bytes
!
! Last configuration change at 09:51:20 JST Mon Dec 2 2024
! NVRAM config last updated at 09:51:20 JST Mon Dec 2 2024
!
version 15.2
no service pad
service timestamps debug datetime msec localtime
service timestamps log datetime msec localtime
service password-encryption
service compress-config
!
hostname TEST-Cat4500X-1
!
boot-start-marker
boot system flash bootflash:cat4500e-universalk9.SPA.XX.XX.XX.X.XXX-X.XXX.bin
boot-end-marker
...途中省略...
line vty 0 4
exec-timeout 0 0
password 7 xxxxxxxxx
login
transport input telnet
line vty 5 16
no login
transport input none
!
ntp server vrf mgmtVrf xx.xx.xx.xx
mac address-table aging-time 86400
!
end
TEST-Cat4500X-1#
投入した設定がきちんと反映されていることが確認できます。
補足1
以下の部分でパスワードをハードコーディングするようになっていますが、本来はファイルから読み込んだり、シークレットマネージャから取得したり、安全な方法をとるのが望ましいです。
const (
remoteHostName = "TEST-Cat4500X-1"
remoteHostAddrPort = "192.168.XX.XX:23"
loginPW = "secret"
enablePW = "secret"
)
環境変数で渡す方法もありますが、環境変数は他のプロセスから見えてしまうため、あまりお薦めできません。それしか方法がない場合のみ使った方が良いでしょう。
補足2
以下の部分は、SpawnGenericを用いてexpectの初期化を行っています。こちらのコードを参考にしています。
https://github.com/google/goexpect/blob/master/examples/newspawner/telnet.go
// expectを開始
resCh := make(chan error)
e, _, err := expect.SpawnGeneric(&expect.GenOptions{
In: conn,
Out: conn,
Wait: func() error {
return <-resCh
},
<snip>
補足3
注意が必要なところとして、ExpectBatchで待ち受ける以下の部分は、正規表現の文字列(string)を渡します。
_, err = e.ExpectBatch([]expect.Batcher{
// パスワードプロンプトを待つ
&expect.BExp{R: passwordPrompt},
一方で、Expectで待ち受ける場合はコンパイル済み正規表現(regexp.Regexp)を渡す必要があります。ここではMustCompileを用いてますが、関数の中で実装する場合はエラーハンドリングをした方がよいでしょう。
resp, _, err := e.Expect(regexp.MustCompile(enablePrompt), timeout)
最後に
ところで、さくらインターネットではエンジニア採用を強化しております。
カジュアル面談もやってます。ご興味ありましたら、お気軽にお声がけいただけると幸いです。最後までお読みいただきありがとうございました。