5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

さくらインターネットAdvent Calendar 2024

Day 2

Go言語でtelnetを自動化してルータの設定を行う

Last updated at Posted at 2024-12-02

はじめに

この記事は さくらインターネット 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 の出力結果を取得する
main.go
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.SpawnGenericexpect.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)

最後に

ところで、さくらインターネットではエンジニア採用を強化しております。

5377ecb4-941b-e505-a1c9-9f2de6b16398.png

カジュアル面談もやってます。ご興味ありましたら、お気軽にお声がけいただけると幸いです。最後までお読みいただきありがとうございました。

5
0
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
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?