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