note.

調べたことの頭の整理

Juniper/go-netconfをざっと触る

Junos の自動化に Python ライブラリの PyEZ があることはご存知(ネットワークエンジニアの世界)の方が多いと思います。最近は Go 言語を触る機会が多いので、ふと調べたらJuniper/go-netconfが出てきたのでざっと触ってみました。

試した際のログはsourjp/lab-networkに置きました。

go-netonf?

This library is a simple NETCONF client based on RFC6241 and RFC6242 (although not fully compliant yet). Note: this is currently pre-alpha release. API and features may and probably will change. Suggestions and pull requests are welcome.

Juniper 公式パッケージで、netconf 接続を go 言語で実装されています。簡単に言えば PyEZ の Go 言語版に位置しますが、実装は比べると少なく、記載の通り pre-alpha です。

基本的な使い方

基本的な使い方は PyEZ と同じ感覚です。

  1. Connection を作成
  2. XML メッセージを投げる

とりあえずサンプルを書きました。

package main

import (
    "fmt"
    "log"

    "github.com/Juniper/go-netconf/netconf"
    "golang.org/x/crypto/ssh"
)

func main() {
    sshConfig := &ssh.ClientConfig{
        User:            "root",
        Auth:            []ssh.AuthMethod{ssh.Password("Juniper")},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    s, err := netconf.DialSSH("localhost:2830", sshConfig)
    if err != nil {
        log.Fatal(err)
    }
    defer s.Close()

    r, err := s.Exec(netconf.RawMethod("<get-chassis-inventory/>"))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Reply: %+v", r)
}
"""
$ go run main.go
[]
0
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<chassis-inventory xmlns="http://xml.juniper.net/junos/12.1X47/junos-chassis">
<chassis junos:style="inventory">
<name>Chassis</name>
...
"""

まず、netconf.DialSSH()でコネクションを作成します。 作成したコネクションのExec()により RPC を通して XML メッセージを送信します。

このExec()で受け取る型はRPCMethodで、実体はstringです。 そしてこの型を作成する方法は次の 4 種類です。

// RawMethod defines how a raw text request will be responded to
type RawMethod string

// MethodLock files a NETCONF lock target request with the remote host
func MethodLock(target string) RawMethod {
    return RawMethod(fmt.Sprintf("<lock><target><%s/></target></lock>", target))
}

// MethodUnlock files a NETCONF unlock target request with the remote host
func MethodUnlock(target string) RawMethod {
    return RawMethod(fmt.Sprintf("<unlock><target><%s/></target></unlock>", target))
}

// MethodGetConfig files a NETCONF get-config source request with the remote host
func MethodGetConfig(source string) RawMethod {
    return RawMethod(fmt.Sprintf("<get-config><source><%s/></source></get-config>", source))
}

このことから次の使い分けをして情報を取得するようです。

メソッド 用途
netconf.RawMethod() show コマンドなど情報を取得したい場合
netconf.MethodLock() 設定を変える場合
netconf.MethodUnlock() MethodLock()をしたい場合は忘れずに
netconf.MethodGetConfig() config を取得する場合

RPC の調べ方

先ほどのサンプルのように情報を取得したい場合は、特定のコマンドが Junos XML API のどれに当たるのかを調べる必要があります。

Junos XML API Explorerで、コマンドで検索すれば一発です。(昔からありましたっけ?)

ただ古い OS を利用しているとマトリクスに存在しないので、(きっと新しい ver と同じでしょうが)調べる方法があります。次のように CMD | display xml rpcとつけることで、何を使っているかがわかります。

root@vsrx1> show interfaces terse | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/12.1X47/junos">
    <rpc>
        <get-interface-information>
                <terse/>
        </get-interface-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

上記の場合であれば、次のようにすることで show interface terse の結果が XML で得ることができます。

r, err := s.Exec(netconf.RawMethod("<get-interface-information><terse/></get-interface-information>"))

応用的な使い方

レポジトリにもいくつかサンプルがあります。

その中で面白かったのが CLI の作成で、ちょっとしたサンプルを書いてみました。

引数で接続先(port 番号)を与えて、取りたい情報を-chassis-ifで指定しています。

$ ./main -chassis -port=2830
Chassis : 340603e5d1fa

$ ./main -chassis -port=2831
Chassis : 8c7c5874f721

$ ./main -if -port=2830
ge-0/0/0 : up
ge-0/0/1 : up
ge-0/0/2 : up

長いので chassis の serial number 取得する部分だけ抜粋します。

func main() {
    target := flag.String("target", "localhost", "target")
    port := flag.String("port", "2830", "port")
    chassisCmd := flag.Bool("chassis", false, "get chassis serial number from 'show chassis hardware'")
    ifCmd := flag.Bool("if", false, "get if admin from 'show interface terse'")

    flag.Parse()
    c, err := NewConn(*target, *port)
    if err != nil {
        log.Fatalln(err)
    }
    defer c.conn.Close()

    if *chassisCmd {
        if err := c.GetChassisSN(); err != nil {
            log.Fatal(err)
        }
    }
    if *ifCmd {
        if err := c.GetIfAdmin(); err != nil {
            log.Fatal(err)
        }
    }
}

// Conn netconf.Session
type Conn struct {
    conn *netconf.Session
}

// NewConn Entrypoint of netconf.Session
func NewConn(target, port string) (Conn, error) {
    conf := &ssh.ClientConfig{
        User:            "root",
        Auth:            []ssh.AuthMethod{ssh.Password("Juniper")},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    d := fmt.Sprintf("%v:%v", target, port)
    s, err := netconf.DialSSH(d, conf)
    if err != nil {
        return Conn{}, err
    }
    return Conn{conn: s}, nil
}

// GetChassisSN get chassis serial number from "show chassis hardware"
func (c *Conn) GetChassisSN() error {
    out, err := c.conn.Exec(netconf.RawMethod("<get-chassis-inventory/>"))
    if err != nil {
        return err
    }
    var ci ChassisInventory
    if err = xml.Unmarshal([]byte(out.Data), &ci); err != nil {
        return err
    }

    chassisName := strings.ReplaceAll(ci.Chassis.Name, "\n", "")
    chassisSN := strings.ReplaceAll(ci.Chassis.SerialNumber, "\n", "")
    fmt.Printf("%v : %v\n", chassisName, chassisSN)

    return nil
}

// ChassisInventory struct for "show chassis hardware"
type ChassisInventory struct {
    XMLName xml.Name `xml:"chassis-inventory"`
    Text    string   `xml:",chardata"`
    Xmlns   string   `xml:"xmlns,attr"`
    Chassis struct {
        Text          string `xml:",chardata"`
        Style         string `xml:"style,attr"`
        Name          string `xml:"name"`
        SerialNumber  string `xml:"serial-number"`
        Description   string `xml:"description"`
        ChassisModule []struct {
            Text             string `xml:",chardata"`
            Name             string `xml:"name"`
            Description      string `xml:"description"`
            ChassisSubModule struct {
                Text        string `xml:",chardata"`
                Name        string `xml:"name"`
                Description string `xml:"description"`
            } `xml:"chassis-sub-module"`
        } `xml:"chassis-module"`
    } `xml:"chassis"`
}

このように Go であれば簡単に CLI ツールを作成できますし、Go 言語の特徴としてクロスコンパイルができるので、作成した運用コマンド集を CLI として配布して、WindowsMAC ユーザーも同じように叩けるっていうのは一つかも、、しれません。 あとは API の作成も簡単ですし、xml の構造体を json に変換してもいいかもしれません。

一番手間がかかるのは Go で構造体を用意することですが、Junos から XML のレスポンスを受けて、XML to Go structに貼り付ければ簡単に作れます。

まとめ

いかがでしょうか?シンプルな実装で使いやすいですね。ただ commit 状況やその他ベンダのサポートも考えると Python ライブラリによる実装が今もメインかもしれません。

しかしながら他の言語で使えるようになると選択肢が増えるのでとてもいいことですね。gobgpもありますし、個人的には Go の方が好きだったりします。

接続が失敗する場合

次のように接続できない場合についてです。

$ go run main.go
2020/12/17 15:27:55 dial tcp [::1]:830: connect: connection refused
exit status 1

PyEZ でもそうですが接続方法が netconf 接続であるため、port 830を利用します。

Junos には次の設定を忘れずにしましょう。

set system services netconf ssh

そして Vagrantfile を利用している方は次の設定を行ってください。

vsrx.vm.network :forwarded_port, id: "ssh", guest: 22, host: 2201 # for SSH
vsrx.vm.network :forwarded_port, id: "netconf", guest: 830, host: 2830 # for NETCONF