技術メモ

ネットワークエンジニアがpython, perl等々を気楽に使うための覚え書き

python jinja2(part1) cisco機器のconfig作成に便利なjinja2.Environment()設定

テンプレートエンジンって便利?

これまで、ネットワーク機器のコンフィグを自動生成するような目的で、テンプレートエンジンを使うのは「大げさ」だと思っていた・・・(簡単なtemplateの利用方法:python標準のstring.Templateを用いたcisco config作成スクリプト - 技術メモ

  • WEBの場合はデザイナ、プログラマの分業に役立つだろうけど、コンフィグ作成程度で分業はないよね。
  • そもそも、テンプレートエンジン独自の命令を覚えるのが面倒。慣れた言語の標準ベースで使える諸機能(ヒアドキュメント、文字列置換等)で十分。

とはいえ、自動生成ツールを作るたび、書き捨てスクリプトの山が増えていくだけの生産性の悪い現状は改善したいと思い、jinja2を試してみた。

試行錯誤

jinja2をインストールして、各種のjinja2入門サイトを参考にしながら"素"のまま使ってみたが、

  • ブラケットを多用する記法はciscoのコンフィグに馴染まないため、テンプレートの可読性が悪い(カーリーブラケットのJUNOSやPAN-OSなら気にならないのだろうけど)。
  • jinja2が勝手に行う、改行と空白制御をコントロールするのが面倒(ハイフン付け)で、結果、タイピング量も多くなりがち。

等々、残念な第一印象・・・。

腰を据えて、jinja2の公式ドキュメント(http://jinja.pocoo.org/docs/dev/)を読み、試行錯誤した結果、

  • trim_blocksを有効化(勝手に空白を詰めたり改行しなくなる)*1
  • ciscoコンフィグの場合、line statementも有効化(ブラケットの羅列からサヨナラ出来る)

とすれば、テンプレートをスッキリと仕上げることが可能なことが分かった。

同時に、pythonらしいふるまいをしてくれる、いくつかの便利な機能も発見。

  • pprintが使える!(デバッグの効率化)
  • 自作のpython関数も組み込み可能(filter等)!
  • 'extension'とやらを有効化すると、テンプレートの中で、よりpythonらしいコントロールが出来そう。

以上を踏まえて・・・

Catalyst3560(8ポートモデル)のL1~L2設定をjinja2で作ってみた。「データをテンプレートに当てはめただけ」の試験にならないよう、jinja2の色々な機能を試せるように、以下の制約を行う。

  • default設定がコンフィグに現れない、IOSのコンフィグ表示機能をjinja2で実装すること(例えば、vlan 1やspeed/duplexのauto設定が表示されないこと)
  • Gi0/1をアップリンクと想定し、jinja2でアクセス側のVlan設定を読み取り、アップリンクのtrunk情報を自動生成すること

jinja2のみでは手を焼きそうな部分は、pythonの自作関数(reorder_vlan, separate_vlanが該当)をjinja2のfilter登録で対応した。

以下、jinja2利用の改善策を適用したお試しスクリプト(第一弾)。

#!/usr/bin/env python3
import re
import ruamel.yaml as yaml
import jinja2

def reorder_vlan(csv):
    """ convert disorderd vlans(csv) into ios configuration format.
        input : " 100 , 2, 3 - 5, 4 , 3,, "
        return: "2-5,100"
    """
    values = split_vlan(csv).split(',')
    csv = values[0]
    for i in range(1, len(values)):
        csv += '-' if int(values[i]) - int(values[i - 1]) == 1 else ','
        csv += values[i]
    return re.sub(r"-(?:\d+-){1,}", '-', csv)

def split_vlan(csv):
    """ split disorderd vlans(csv), and uniq|sort them out.
        input : " 100 , 2, 3 - 5, 4 , 3,, "
        return: "2,3,4,5,100"
    """
    re_number = re.compile(r"^(?:\s*(\d+)\s*|\s*(\d+)\s*-\s*(\d+)\s*)$")
    values = set()
    for val in csv.split(','):
        mo = re_number.search(val)
        if mo:
            if mo.group(1):
                values.add(int(mo.group(1)))
            else:
                values.update(range(int(mo.group(2)), int(mo.group(3)) + 1))
    return ','.join(map(str, sorted(values)))

yaml_data = """
FastEthernet0/1:
    vlan: 1
    speed: auto
    duplex: auto
FastEthernet0/2:
    description: ""
    vlan: 1
    duplex: ""
FastEthernet0/3:
    description: port0/3
    vlan: 2
    speed: 10
    duplex: full
FastEthernet0/4:
    description: port0/4
    vlan: 102
    speed: 10
    duplex: half
FastEthernet0/5:
    description: port0/5
    vlan: 101
    speed: 10
    duplex: half
FastEthernet0/6:
    description: port0/6
    vlan: 100
    speed: 10
    duplex: half
FastEthernet0/7:
    description: port0/7
    vlan: 10
    speed: auto
    duplex: auto
FastEthernet0/8:
    description: port0/8
    vlan: 3
    speed: 100
    duplex: full
"""

dict_ = yaml.load(yaml_data, Loader = yaml.RoundTripLoader)

j2_template = """\
{#
{{ intf_phy | pprint }}
#}
!
! interface configuration(C3560-8PC-S)
!
vtp mode off
!
{% set vlan_csv =
    intf_phy.values() | map(attribute = 'vlan') | join(',') | split_vlan
%}
vlan {{ vlan_csv.lstrip('1,') | reorder_vlan }}
!
% for k, v in intf_phy.items():
interface {{ k }}
% if v.description:
 description {{ v.description }}
% endif
% if v.vlan and v.vlan != 1:
 switchport access vlan {{ v.vlan }}
% endif
 switchport mode access
% if v.speed and v.speed != "auto":
 speed {{ v.speed }}
% endif
% if v.duplex and v.duplex != "auto":
 duplex {{ v.duplex }}
% endif
!
% endfor
interface GigabitEthernet0/1
 description uplink
 switchport trunk encapsulation dot1q
 switchport trunk allowed vlan {{ vlan_csv | reorder_vlan }}
 switchport mode trunk
!"""

j2_env = jinja2.Environment(
    line_statement_prefix   = '%',
    line_comment_prefix     = '%%',
    trim_blocks             = True,
    keep_trailing_newline   = True,
    extensions              = ['jinja2.ext.do', 'jinja2.ext.loopcontrols'],
)
j2_env.filters.update({
    'reorder_vlan'  : reorder_vlan,
    'split_vlan'    : split_vlan,
})

t = j2_env.from_string(j2_template)
print(t.render(intf_phy = dict_))

以下、実行結果。

!
! interface configuration(C3560-8PC-S)
!
vtp mode off
!
vlan 2-3,10,100-102
!
interface FastEthernet0/1
 switchport mode access
!
interface FastEthernet0/2
 switchport mode access
!
interface FastEthernet0/3
 description port0/3
 switchport access vlan 2
 switchport mode access
 speed 10
 duplex full
!
interface FastEthernet0/4
 description port0/4
 switchport access vlan 102
 switchport mode access
 speed 10
 duplex half
!
interface FastEthernet0/5
 description port0/5
 switchport access vlan 101
 switchport mode access
 speed 10
 duplex half
!
interface FastEthernet0/6
 description port0/6
 switchport access vlan 100
 switchport mode access
 speed 10
 duplex half
!
interface FastEthernet0/7
 description port0/7
 switchport access vlan 10
 switchport mode access
!
interface FastEthernet0/8
 description port0/8
 switchport access vlan 3
 switchport mode access
 speed 100
 duplex full
!
interface GigabitEthernet0/1
 description uplink
 switchport trunk encapsulation dot1q
 switchport trunk allowed vlan 1-3,10,100-102
 switchport mode trunk

*1:jinja2を採用しているansibleでもtrim_blocksを有効化しているらしい