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