在网络项目开局的时候,无论是采用 ZTP 还是人工敲命令,都需要准备整网各个 Role 的设备配置文件。如果人工地对配置模版中的字符进行逐个替换,工作量太大了,而且也很容易出错。最好的办法就是用配置模版自动化地生成设备配置文件。
如果使用 Python 语言,则可以使用 Jinja2 来帮助我们完成这项工作。
Jinja2 是一个通用的库,应用非常广泛,可以用模版和若干变量的 Key-Value 输出任何文本,当然也包括网络设备的配置文件。
对于配置文件中,需要变化的部分,我们可以把它们设置成变量,用两个大括号 {{ }} 括起来。最好前后都留一个空格,这样看起来清晰美观。所有的变量的值,我们可以用 Python 的字典来存储。
例如,我们要配置一个 SVI,下面是一个简单的例子:
#!/usr/bin/env python3# Example 1import jinja2my_vars = {'VLAN_ID': 200,'VLAN_NAME': 'Route-Access','SVI_IP': '10.0.0.1','OSPF_PWD': 'Password','BR_PRY': 255,'OSPF_PRCS': 200,'OSPF_AREA': 200}my_template = '''interface Vlan{{ VLAN_ID }}description {{ VLAN_NAME }}no ip redirectsno ipv6 redirectsip address {{ SVI_IP }}/25ip ospf authentication message-digestip ospf message-digest-key 1 md5 {{ OSPF_PWD }}ip ospf dead-interval 4ip ospf hello-interval 1ip ospf priority {{ BR_PRY }}ip router ospf {{ OSPF_PRCS }} area {{ OSPF_AREA }}no shutdown'''t = jinja2.Template(my_template)print(t.render(my_vars))
执行这个 Python 脚本之后,会得到以下输出,我们得到了完整的 interface Vlan200 的配置。
interface Vlan200description Route-Accessno ip redirectsno ipv6 redirectsip address 10.0.0.1/25ip ospf authentication message-digestip ospf message-digest-key 1 md5 Passwordip ospf dead-interval 4ip ospf hello-interval 1ip ospf priority 255ip router ospf 200 area 200no shutdown
下一个问题是,在很多 Role 会有 2 台设备做冗余,在配置文件相同的位置,2 台设备的配置参数是不一样的。例如 VPC/HSRP/STP 的配置,或者上面例子中的 OSPF BR 优先级。我们当然可以为变量赋不同的值,但这样就会产生非常多的变量。
为了解决这个问题,我们可以增加一个变量:Modul ID。然后利用 Jinja2 的条件判断语句,来生成不同的配置。这样可以减少变量的数量。
Jinja2 的条件判断语句的格式为:
{% if Condition1 -%}Code block 1{% elif Condition2 -%}Code block 2{% else -%}Code block n{% endif -%}
有几点需要注意:
变量的符号是 {{ }},但条件判断和接下来要介绍的循环语句的格式是 {% %}
如果直接用 {% %} 来生成配置文件,该代码块的前后会分别有一个空行。如果不想要多余的空行,就要加一个中横线。可以加在第 1 个百分号的后面,也可以加在第 2 个百分号的前面
在代码块的结尾,必须使用 {% endif %} 声明该条件判断结束
下面我们改写 Python 脚本,为第 2 台设备生成 interface Vlan 的配置。
#!/usr/bin/env python3# Example 2import jinja2my_vars = {'MOD': 2,'VLAN_ID': 200,'VLAN_NAME': 'Route-Access','SVI_IP': '10.0.0.2','OSPF_PWD': 'Password','OSPF_PRCS': 200,'OSPF_AREA': '0.0.0.200'}my_template = '''interface Vlan{{ VLAN_ID }}description {{ VLAN_NAME }}no ip redirectsno ipv6 redirectsip address {{ SVI_IP }}/25ip ospf authentication message-digestip ospf message-digest-key 1 md5 {{ OSPF_PWD }}ip ospf dead-interval 4ip ospf hello-interval 1{%- if MOD == 1 %}ip ospf priority 255{%- else %}ip ospf priority 254{%- endif %}ip router ospf {{ OSPF_PRCS }} area {{ OSPF_AREA }}no shutdown'''t = jinja2.Template(my_template)print(t.render(my_vars))
执行这个脚本,我们可以看到 OSPF BR 优先级不再是 255,而是 254。但我们不再需要单独定义一个 ospf_priority 变量了。
interface Vlan200description Route-Accessno ip redirectsno ipv6 redirectsip address 10.0.0.2/25ip ospf authentication message-digestip ospf message-digest-key 1 md5 Passwordip ospf dead-interval 4ip ospf hello-interval 1ip ospf priority 254ip router ospf 200 area 0.0.0.200no shutdown
我们再丰富一下脚本的内容。首先,判断接口是否属于某个 VRF,再判断是否需要启用 IPv6。如果答案为 True,则增加相应的配置。
#!/usr/bin/env python3# Example 3import jinja2my_vars = {'MOD': 2,'VLAN_ID': 200,'VLAN_NAME': 'Route-Access','VRF': False,'VRF_NAME': 'my_vrf','SVI_IP': '10.0.0.2','OSPF_PWD': 'Password','OSPF_PRCS': 200,'OSPF_AREA': '0.0.0.200','IPV6': True,'SVI_IPV6': '2001::1'}my_template = '''interface Vlan{{ VLAN_ID }}description {{ VLAN_NAME }}{%- if VRF %}vrf member {{ VRF_NAME }}{%- endif %}no ip redirectsno ipv6 redirectsip address {{ SVI_IP }}/25ip ospf authentication message-digestip ospf message-digest-key 1 md5 {{ OSPF_PWD }}ip ospf dead-interval 4ip ospf hello-interval 1{%- if MOD == 1 %}ip ospf priority 255{%- else %}ip ospf priority 254{%- endif %}ip router ospf {{ OSPF_PRCS }} area {{ OSPF_AREA }}{%- if IPV6 %}ipv6 address {{ SVI_IPV6 }}/64ipv6 router ospfv3 {{ OSPF_PRCS }} area {{ OSPF_AREA }}ipv6 nd reachable-time 1000{%- if MOD == 1 %}ospfv3 priority 255{%- else %}ospfv3 priority 254{%- endif %}ospfv3 hello-interval 1ospfv3 dead-interval 4ospfv3 mtu-ignore{%- endif %}no shutdown'''t = jinja2.Template(my_template)print(t.render(my_vars))
上面的脚本使用了 4 段 if 语句,其中 1 个是嵌套的 if。Jinja2 不像 Python 那样依赖代码缩进,它会自动判断多段 if 和嵌套 if 的开始和结束。但是出于代码可读性的考虑,最好还是为嵌套语句增加缩进,这样看起来清晰一些。
Example 3 执行之后,得到以下输出:
interface Vlan200description Route-Accessno ip redirectsno ipv6 redirectsip address 10.0.0.2/25ip ospf authentication message-digestip ospf message-digest-key 1 md5 Passwordip ospf dead-interval 4ip ospf hello-interval 1ip ospf priority 254ip router ospf 200 area 0.0.0.200ipv6 address 2001::1/64ipv6 router ospfv3 200 area 0.0.0.200ipv6 nd reachable-time 1000ospfv3 priority 254ospfv3 hello-interval 1ospfv3 dead-interval 4ospfv3 mtu-ignoreno shutdown
因为变量 VRF 的值是 False,所以没有配置 vrf member。但因为 IPV6 的值是 True,所以输出了 ipv6 和 ospfv3 的配置。
上面是 Jinja2 条件判断的使用方法。下面我们讲讲循环语句的使用方法。
在生产网络中,设备不会只有 1 个接口,通常会有很多接口使用相似的配置。比如上面例子中的 SVI 接口,或者多个 OSPF 进程的配置,或者 TOR 交换机连接服务器的接口配置。我们当然可以把完整的配置模版写出来,但如果那样,配置模版的行数就太多了。比较好的解决办法是使用循环语句来减少配置模版的行数。
另外,对于一个复杂的工作,有必要将变量和模版放到不同的文件中存储,让项目变得结构化。同时,因为变量越来越多,字典也不再是合适的存储变量的容器,我们使用 YAML 文件来存储变量。因为篇幅的关系,就不单独介绍 YAML 了,大家只要记住 2 点:
如果前面有中横线 "-",说明这是一个 list
如果前面没有中横线,说明这是一个字典的 key: value
首先准备变量表。我们这次用一个 YAML 文件存储变量。需求是:
vlan100 配置 vrf member,ipv4,ipv6,ospf,ospfv3
vlan200 配置 ipv4 和 ospf,不配置其他项目
vlan300 配置 vrf member,ipv4,ipv6,但不配置 ospf 和 ospfv3
---'module_id': 2'vlan_interfaces':'vlan100':'name': 'VLAN_NAME_100''vrf': 'VRF1''ip_addr': '10.0.100.1''ip_mask': 25'ospf': True'ospf_prcs': 100'ospf_pwd': 'Password''ospf_area': 100'ipv6_addr': '2001::1''ipv6_mask': 64'ospfv3': True'ospfv3_prcs': 100'ospfv3_area': 100'Vlan200':'name': 'VLAN_NAME_200''vrf': '''ip_addr': '10.0.200.1''ip_mask': 25'ospf': True'ospf_prcs': 200'ospf_pwd': 'Password''ospf_area': 200'ipv6_addr': '''Vlan300':'name': 'VLAN_NAME_300''vrf': 'VRF3''ip_addr': '10.0.300.1''ip_mask': 25'ospf': False'ipv6_addr': '2003::1''ipv6_mask': 64'ospfv3': False
然后写一个 Jinja2 模版。我加了一些注释。请注意,注释的符号是 {# #}。如果不希望注释产生多余的空行,要加上中横线 {#- #} 或者 {# -#}。另外,必须用 {% endfor %} 显式声明一段循环语句的结束。我没有为 endfor 加中横线,所以预期每个 interface vlan 的配置下面都会有一个空行,看起来美观一些。
{#- 遍历所有的 vlan interface #}{%- for intf_name, intf_dict in vlan_interfaces.items() %}interface {{ intf_name }}description {{ intf_dict.name }}{#- 判断是否需要为接口配置 VRF #}{%- if intf_dict.vrf != '' %}vrf member {{ intf_dict.vrf }}{%- endif %}no ip redirectsip address {{ intf_dict.ip_addr }}/{{intf_dict.ip_mask}}{#- 判断是否需要配置 OSPF #}{%- if intf_dict.ospf == True %}ip ospf authentication message-digestip ospf message-digest-key 1 md5 {{ intf_dict.ospf_pwd }}ip ospf dead-interval 4ip ospf hello-interval 1{#- 判断 OSPF BR 优先级 #}{%- if module_id == 1 %}ip ospf priority 255{%- else %}ip ospf priority 254{%- endif %}ip router ospf {{ intf_dict.ospf_prcs }} area 0.0.0.{{ intf_dict.ospf_area }}{%- endif %}{#- 判断是否需要为接口配置 IPV6 #}{%- if intf_dict.ipv6_addr != '' %}no ipv6 redirectsipv6 address {{ intf_dict.ipv6_addr }}/{{ intf_dict.ipv6_mask }}ipv6 nd reachable-time 1000{#- 判断是否需要为接口配置 OSPFv3 #}{%- if intf_dict.ospfv3 == True %}ipv6 router {{ intf_dict.ospfv3_prcs }} area 0.0.0.{{ intf_dict.ospfv3_area }}{#- 判断 OSPFv3 BR 优先级 #}{%- if module_id == 1 %}ospfv3 priority 255{%- else %}ospfv3 priority 254{%- endif %}ospfv3 hello-interval 1ospfv3 dead-interval 4ospfv3 mtu-ignore{%- endif %}{%- endif %}no shutdown{% endfor %}
最后,写一个 Python 脚本。这次我们把输出写入一个文本文件而不是直接打印:
#!/usr/bin/env python3# Example 4import yamlimport jinja2yaml_file = 'example4.yml'with open(yaml_file) as f:template_vars = yaml.load(f, Loader=yaml.FullLoader)jinja_file = 'example4.j2'with open(jinja_file) as f:template = jinja2.Template(f.read())output_file = 'example4.txt'with open(output_file, 'w') as f:f.write(template.render(template_vars))
运行这个 Python 脚本,成功生成 example4.txt 文件。内容如下:
interface vlan100description VLAN_NAME_100vrf member VRF1no ip redirectsip address 10.0.100.1/25ip ospf authentication message-digestip ospf message-digest-key 1 md5 Passwordip ospf dead-interval 4ip ospf hello-interval 1ip ospf priority 254ip router ospf 100 area 0.0.0.100no ipv6 redirectsipv6 address 2001::1/64ipv6 nd reachable-time 1000ipv6 router 100 area 0.0.0.100ospfv3 priority 254ospfv3 hello-interval 1ospfv3 dead-interval 4ospfv3 mtu-ignoreno shutdowninterface Vlan200description VLAN_NAME_200no ip redirectsip address 10.0.200.1/25ip ospf authentication message-digestip ospf message-digest-key 1 md5 Passwordip ospf dead-interval 4ip ospf hello-interval 1ip ospf priority 254ip router ospf 200 area 0.0.0.200no shutdowninterface Vlan300description VLAN_NAME_300vrf member VRF3no ip redirectsip address 10.0.300.1/25no ipv6 redirectsipv6 address 2003::1/64ipv6 nd reachable-time 1000no shutdown
Done.




