LGTM

Looks Good To Me

junoser internals

junoser というRuby gem をメンテしている. これはJUNOS Config のPEG パーサーで,標準入力やファイルを JUNOS Config として構文解析できる.解析後,文法チェックやスタイルの変換もできる.

たとえば,

構文チェック.display | set 形式とデフォルトの構造化スタイル両方をサポート.

$ junoser -c config.invalid
Invalid syntax:  set protocols bgp group e1 neighbor 10.0.26.1 exprot s2b
zsh: exit 1     junoser -c config.invalid

スタイルの変換.display | set スタイル → 構造化スタイル

junoser -s config.set

スタイルの変換.構造化スタイル → display | set スタイル

junoser -d config

さっとチェックしたり,CI に使ったり,レビュー時 display | set スタイルがつらい時に重宝してる.

文法定義はどこから来るか

CLI と,NETCONF 用のXML が構造的に似ていることを利用して,<get-schema> で取れるXML Schema(XSD) を使っている.

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" xmlns:junos="http://xml.juniper.net/junos/12.1X47/junos">
  <xsd:import schemaLocation="junos.xsd" namespace="http://xml.juniper.net/junos/12.1X47/junos"/>
  <xsd:element name="configuration">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:choice minOccurs="0" maxOccurs="unbounded">
          <xsd:element name="interfaces" minOccurs="0">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:choice minOccurs="0" maxOccurs="unbounded">
                  <xsd:element name="interface" minOccurs="0" maxOccurs="unbounded" type="interfaces-type">
                  </xsd:element>
                </xsd:choice>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>
  <xsd:complexType name="interfaces-type">
    <xsd:sequence>
      <xsd:element name="name">
        <xsd:complexType>
          <xsd:simpleContent>
            <xsd:restriction base="key-attribute-string-type">
              <xsd:enumeration value="$junos-interface-ifd-name">
              </xsd:enumeration>
              <xsd:enumeration value="interface-name">
              </xsd:enumeration>
            </xsd:restriction>
          </xsd:simpleContent>
        </xsd:complexType>
      </xsd:element>
      <xsd:choice minOccurs="0" maxOccurs="unbounded">
        <xsd:element name="unit" minOccurs="0" maxOccurs="unbounded">
          <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="name">
                <xsd:complexType>
                  <xsd:simpleContent>
                    <xsd:restriction base="key-attribute-ulong-type">
                      <xsd:enumeration value="$junos-underlying-interface-unit">
                      </xsd:enumeration>
                      <xsd:enumeration value="$junos-interface-unit">
                      </xsd:enumeration>
                      <xsd:enumeration value="interface-unit-number">
                      </xsd:enumeration>
                    </xsd:restriction>
                  </xsd:simpleContent>
                </xsd:complexType>
              </xsd:element>
              <xsd:choice minOccurs="0" maxOccurs="unbounded">
                <xsd:element name="family" minOccurs="0">
                  <xsd:complexType>
                    <xsd:sequence>
                      <xsd:choice minOccurs="0" maxOccurs="unbounded">
                        <xsd:element name="inet" minOccurs="0">
                          <xsd:complexType>
                            <xsd:sequence>
                              <xsd:choice minOccurs="0" maxOccurs="unbounded">
                                <xsd:element name="address" minOccurs="0" maxOccurs="unbounded">
                                  <xsd:complexType>
                                    <xsd:sequence>
                                      <xsd:element name="name">
                                        <xsd:complexType>
                                          <xsd:simpleContent>
                                            <xsd:extension base="ipv4prefix">
                                              <xsd:attribute name="key" type="xsd:string" fixed="key"/>
                                            </xsd:extension>
                                          </xsd:simpleContent>
                                        </xsd:complexType>
                                      </xsd:element>
                                    </xsd:sequence>
                                  </xsd:complexType>
                                </xsd:element>
                              </xsd:choice>
                            </xsd:sequence>
                          </xsd:complexType>
                        </xsd:element>
                      </xsd:choice>
                    </xsd:sequence>
                  </xsd:complexType>
                </xsd:element>
              </xsd:choice>
            </xsd:sequence>
          </xsd:complexType>
        </xsd:element>
      </xsd:choice>
    </xsd:sequence>
  </xsd:complexType>
</xsd:schema>

上記のXSD はごく一例*1だが,XML パーサーを使って,これを下のruby コードに変換する. (junoser に含めたパーサージェネレーターで自動生成)

module Junoser
  class Parser < Parslet::Parser
    rule(:configuration) do
      c(
        b(str("interfaces"),
          c(
            interfaces_type
          )
        )
      )
    end

    rule(:interfaces_type) do
      b((arg | str("interface-name")).as(:arg),
        c(
          a(str("unit"), arg | arg | str("interface-unit-number"),
            c(
              b(str("family"),
                c(
                  b(str("inet"),
                    c(
                      b(a(str("address"), arg))
                    )
                  )
                )
              )
            )
          )
        )
      )
    end
  end
end

全体で60万行くらいのXSD が,4万行くらいのPEG パーサーになる.これがjunoser の本体だが,パーサージェネレーターの生成物でもある.

パーサージェネレーターの闇

ここから本題です.ここまでは比較的スッキリと書ける.処理する対象がゴチャゴチャしているため一見ややこしいが,やっていることはシンプル.

問題1: CLI とXSD の微妙なちがい

interfaces {
    ge-0/0/0 {
        description "interface 1";
    }
}

上のようなJUNOS Config がある.よく見ますね. description ステートメントの後ろに文字列が来る. これをdisplay | xml すると,

<interfaces>
    <interface>
        <name>ge-0/0/0</name>
        <description>interface 1</description>
    </interface>
</interfaces>

なるほど,<description/> 要素に文字列が入っている.

routing-options {
    autonomous-system 64500;
}

いっぽう,上のようなJUNOS Config がある. autonomous-system ステートメントの後ろに文字列が来る. これをdisplay | xml すると,

<routing-options>
    <autonomous-system>
        <as-number>64600</as-number>
    </autonomous-system>
</routing-options>

なるほど,こちらは<autonomous-system/> 要素の中で,新たに<as-number/> 要素で文字列を包んでいて interface description と違う.

では2つのXSD を見てみましょう.

<xsd:complexType name="interfaces-type">
  <xsd:sequence>
    <xsd:choice minOccurs="0" maxOccurs="unbounded">
      <xsd:element name="description" minOccurs="0" type="xsd:string"/>
    </xsd:choice>
  </xsd:sequence>
</xsd:complexType>
<xsd:element name="autonomous-system" minOccurs="0">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:choice minOccurs="0" maxOccurs="unbounded">
        <xsd:element name="as-number" type="xsd:string"/>
      </xsd:choice>
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

ほぼ同じなわけです.「as-number はただのラベルであって,CLI 上には現れないこと」がXSD からは分からない.

junoser では文脈をくみとりながら「これはただのラベル要素」「これは必要な要素」と選別していて,そこが泥臭いハックになっている.

問題2: そもそもXSD にない定義

junoser はvSRX のXSD をベースにしているため,vSRX にない機能はパースできない.

<xsd:element name="service-set" minOccurs="0" maxOccurs="unbounded">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:choice minOccurs="0" maxOccurs="unbounded"/>
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

たとえば,上はset services service-set ... に対応するXSD だが,vSRX には定義が存在しない.

問題3: ライセンス

XSD で定義される構文木に泥臭いハックを入れて,CLI にあてはめるためのパーサーを作った.このコードのライセンス,微妙ですよね. いちおうJuniper のかたに確認して公開しているが…大丈夫かな? 詳しいかたがいればアドバイスいただけると嬉しいです.

Juniper はよくてもCisco がNG とかだと,IOS-XR 版が作れなくて困ってしまう.

あと,junoser は個人プロジェクトとして進めているので,Juniper にフィードバックしたくてもできないことがあって困ってる.

おわりに

junoser をメンテするにあたって,けっこう泥臭いことをやっています.でも,けっこう動くレベルのはずです.

もし興味をもってもらえたら ぜひ使ってみてください.パースできないCLI コマンドがあれば,フィードバックよろしくお願いします.自分ではあまり使わないConfig については,問題1 を潰しきれていません.

また,手元にXSD がないのが原因で動かないケース(問題2) もあるので,その際にはXSD の提供に協力してもらえると助かります.

こちらが安定してきたら,IOS-XR 版にも着手予定です.

*1:関係のない要素も省略