技術メモ

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

python jinja2(part2) テンプレート継承を用いたcisco機器のconfig自動作成

ビジネス層とプレゼンテーション層

part1でjinja2は便利使えることが分かったものの、テンプレートのスッキリ感は今一つ・・・。改めてテンプレートエンジンの使い方を調べてみた。

なるほど、ビジネス層とプレゼンテーション層を意識してやるのがよさそう。ciscoのコンフィグを作る場合には、こちらの記事が参考になる感じ・・・。

ということで、テンプレートの分割(継承)を試してみる。

jinja2継承テストのシナリオ

以下のシナリオでテンプレートを作成。

  • 支社で使われるcisco製WANルータ(C892FSP)を想定。
  • 冗長性を考慮して拠点は2台のルータ(-01, -02)を使用し、ルーティングプロトコルはEIGRPを、LANのゲートウェイ冗長化にはHSRPを採用。
  • 2台のルータは、それぞれ異なるWAN回線に接続されることとし、ロードバランスさせるため、奇数VLANは1面を、偶数VLANは2面を優先して使うようにHSRPのプライオリティとEIGRPのメトリックを調整。
  • EIGRPは、拠点LANのみアナウンスするようにdistribute-listを設定(prefix-listで定義)。

part1と同様、テンプレートに与えるデータは必要最小限とし、vlanのリスト、EIGRPのnetworkコマンド、prefix-list等は、テンプレートで自動生成させる。さらに、EIGRPのnetworkコマンドは、実機に投入した場合と同様、sortしてから表示する(これも、テンプレート内で実装)。

jinja2継承テストのスクリプトYAMLデータ

(まだ課題はあるものの)ちょっとスッキリした成果物がこちら。

  • ./ios_config_generator.py (jinja2の設定をまとめたモジュール、自作関数もここにまとめる)
  • ./sample.py (YAMLの読み込みとios_config_generatorモジュールの呼び出しを行うスクリプト、これは使い捨て想定)
  • ./j2/config_contents.j2 (子テンプレート、ここに、ios config.に関する一般的な機能をまとめる)
  • ./j2/config_C892FSP-K9.j2 (親テンプレート、利用条件や機種に依存する表現はなるべくこちらにまとめる)
  • ./router.yaml (YAMLデータ)

./ios_config_generator.py

#!/usr/bin/env python3
import os
import re
import ipaddress
import jinja2

def template(tmpl_file, tmpl_dir = os.path.dirname(__file__)):
    j2_loader = jinja2.FileSystemLoader(searchpath = tmpl_dir)
    j2_env = jinja2.Environment(
        loader                  = j2_loader,
        undefined               = jinja2.StrictUndefined,
        line_statement_prefix   = '%',
        line_comment_prefix     = '%%',
        trim_blocks             = True,
        keep_trailing_newline   = True,
        extensions              = ['jinja2.ext.do', 'jinja2.ext.loopcontrols'],
    )
    j2_env.filters.update({
        'with_netmask'  : to_network_with_netmask,
        'with_hostmask' : to_network_with_hostmask,
        'with_wildcard' : to_network_with_hostmask,
        'with_prefix'   : to_network_with_prefix,
        'reorder_vlan'  : reorder_vlan,
        'split_vlan'    : split_vlan,
    })
    j2_env.globals.update({
        'sort_by_network' : sort_by_network,
    })
    return j2_env.get_template(tmpl_file)

def to_network(masked_ip):
    return ipaddress.ip_interface(masked_ip.replace(' ', '/')).network
def to_network_with_prefix(masked_ip):
    return to_network(masked_ip).with_prefixlen
def to_network_with_netmask(masked_ip):
    return to_network(masked_ip).with_netmask.replace('/', ' ')
def to_network_with_hostmask(masked_ip):
    return to_network(masked_ip).with_hostmask.replace('/', ' ')
def sort_by_network(masked_ip_list, method = 'netmask'):
    sorted_net = set(sorted([
        to_network(masked_ip) for masked_ip in masked_ip_list
    ]))
    if 'prefix' in method:
        return [net.with_prefixlen.replace('/', ' ') for net in sorted_net]
    elif 'netmask' in method:
        return [net.with_netmask.replace('/', ' ') for net in sorted_net]
    elif 'host' in method or 'wild' in method:
        return [net.with_hostmask.replace('/', ' ') for net in sorted_net]
    else:
        return sorted_net

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)))

./sample.py

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

yaml_file = 'router_parameter.yml'
tmpl_file = 'config_contents.j2'
tmpl_dir = r'.\j2'

with open(yaml_file, 'r') as f:
    dict_ = yaml.load(f, Loader = yaml.RoundTripLoader)
    # import pprint
    # pprint.pprint(dict_)

t = ios_config_generator.template(tmpl_file, tmpl_dir)
for k, v in dict_.items():
    with open('config_' + k + '.txt', 'w') as f:
        f.write(t.render(v, hostname = k))

./j2/config_contents.j2

% extends "config_C892FSP-K9.j2"

% macro get_vlan_csv(intf_vlan)
{{ intf_vlan.keys() | join(',') | split_vlan }}
% endmacro

% block unnamed_vlan
% set csv = get_vlan_csv(intf_vlan).lstrip('1,') | reorder_vlan
% if csv:
vlan {{ csv }}
% endif
% endblock

% block intf_phy
% set csv = get_vlan_csv(intf_vlan) ~ ',1,1002-1005'
interface GigabitEthernet0
 description LAN switch
 switchport trunk allowed vlan {{ csv | reorder_vlan }}
 switchport mode trunk
 no ip address
{{ super() | trim }}
interface GigabitEthernet9
 no ip address
 no shutdown
% if intf_wan.speed
 duplex full
 speed {{ intf_wan.speed }}
% else
 duplex auto
 speed auto
% endif
!
interface GigabitEthernet9.{{ intf_wan.vlan_id }}
 description {{ intf_wan.desc }}
 encapsulation dot1Q {{ intf_wan.vlan_id }}
 ip address {{ intf_wan.ip_address }}
 no ip redirects
 no ip proxy-arp
% endblock

% block intf_vlan
% for k, v in intf_vlan | dictsort:
% if loop.index0 == 0 and k != 1:
interface Vlan1
 no ip address
 shutdown
!
% endif
interface Vlan{{ k }}
 description {{ v.desc }}
 ip address {{ v.ip_address }}
 no ip redirects
 no ip proxy-arp
% set g = v.standby_group
% if ('-01' in hostname and k is odd) or ('-02' in hostname and k is even):
 standby {{ g }} ip {{ v.standby_ip }}
 standby {{ g }} priority 120
 standby {{ g }} preempt
 standby {{ g }} track 1 decrement 10
 delay 100
% else
 standby {{ g }} ip {{ v.standby_ip }}
 standby {{ g }} priority 115
 standby {{ g }} preempt
 delay 200
% endif
!
% endfor
% endblock

% block track
track 1 interface GigabitEthernet9.{{ intf_wan.vlan_id }} ip routing
% endblock

% block eigrp
router eigrp 10
 distribute-list prefix out_filter out GigabitEthernet9.{{ intf_wan.vlan_id }}
% set network_list = [intf_wan.ip_address]
% do network_list.extend(intf_vlan.values() | map(attribute = 'ip_address'))
% for item in sort_by_network(network_list, 'wildcard'):
 network {{ item }}
% endfor
% endblock

% block prefix_list
% for v in intf_vlan.values():
ip prefix-list out_filter seq {{ loop.index * 5 }} permit {{
    v.ip_address | with_prefix
}}
% endfor
% endblock

./j2/config_C892FSP-K9.j2

service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
service password-encryption
!
hostname {{ hostname }}
!
boot-start-marker
boot-end-marker
!
!
logging buffered 40960
enable secret {{ password.enable }}
!
no aaa new-model
clock timezone JST 9 0
!
!
no ip source-route
ip cef
!
!
!
!


!
!
!
!
no ip domain lookup
no ipv6 cef
!
!
!
!
vtp mode transparent
!
!
!
!
!
% block unnamed_vlan
% endblock
!
!
% block track
% endblock
!
!
!
!
!
!
!
!
!
% block intf_phy
!
% for i in range(1, 8):
interface GigabitEthernet{{ i }}
 no ip address
 shutdown
!
% endfor
interface GigabitEthernet8
 no ip address
 shutdown
 duplex auto
 speed auto
!
% endblock
!
% block intf_vlan
% endblock
!
% block eigrp
% endblock
!
ip forward-protocol nd
no ip http server
no ip http secure-server
!
!
!
% block standard_ip_acl
% endblock
% block extended_ip_acl
% endblock
!
% block prefix_list
% endblock
% block standard_acl
% endblock
% block extended_acl
% endblock
no cdp run
!
!
control-plane
!
!
!
line con 0
 password {{ password.vty }}
 login
 no modem enable
line aux 0
 no exec
line vty 0 4
 password {{ password.vty }}
 login
 transport input telnet
!
scheduler allocate 20000 1000
!
end

./router_parameter.yml

tky-rt-01:
    password: &default_password
        vty: "<<fake_pass>>"
        enable: "##fake_pass##"
    intf_wan: &wan_1
        speed: 10
        vlan_id: 901
        desc: tagged WAN 1
        ip_address: 10.240.1.1 255.255.255.128
    intf_vlan:
        1: &Tokyo_vlan1
            desc: Tokyo office LAN
            ip_address: 10.1.1.252 255.255.255.0
            standby_ip: 10.1.1.254
            standby_group: 1
        2: &Tokyo_vlan2
            desc: Tokyo office LAN
            ip_address: 10.1.2.252 255.255.255.0
            standby_ip: 10.1.2.254
            standby_group: 2
        3: &Tokyo_vlan3
            desc: Tokyo office LAN
            ip_address: 10.1.3.252 255.255.255.0
            standby_ip: 10.1.3.254
            standby_group: 3
        4: &Tokyo_vlan4
            desc: Tokyo office LAN
            ip_address: 10.1.4.252 255.255.255.0
            standby_ip: 10.1.4.254
            standby_group: 4

tky-rt-02:
    password:
        <<: *default_password
    intf_wan: &wan_2
        speed: 10
        vlan_id: 902
        desc: tagged WAN 2
        ip_address: 10.240.2.1 255.255.255.128
    intf_vlan:
        1:
            <<: *Tokyo_vlan1
            ip_address: 10.1.1.253 255.255.255.0
        2:
            <<: *Tokyo_vlan2
            ip_address: 10.1.2.253 255.255.255.0
        3:
            <<: *Tokyo_vlan3
            ip_address: 10.1.3.253 255.255.255.0
        4:
            <<: *Tokyo_vlan4
            ip_address: 10.1.4.253 255.255.255.0

osk-rt-01:
    password:
        <<: *default_password
    intf_wan:
        <<: *wan_1
        ip_address: 10.240.1.2 255.255.255.128
    intf_vlan:
        32: &Osaka_vlan32
            desc: Osaka office LAN
            ip_address: 10.1.32.252 255.255.255.0
            standby_ip: 10.1.32.254
            standby_group: 32
        33: &Osaka_vlan33
            desc: Osaka office LAN
            ip_address: 10.1.33.252 255.255.255.0
            standby_ip: 10.1.33.254
            standby_group: 33

osk-rt-02:
    password:
        <<: *default_password
    intf_wan:
        <<: *wan_2
        ip_address: 10.240.2.2 255.255.255.128
    intf_vlan:
        32:
            <<: *Osaka_vlan32
            ip_address: 10.1.32.253 255.255.255.0
        33:
            <<: *Osaka_vlan33
            ip_address: 10.1.33.253 255.255.255.0