暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

使用 Jinja2 模版生成网络设备配置文件

非资深老网工 2021-03-08
286

在网络项目开局的时候,无论是采用 ZTP 还是人工敲命令,都需要准备整网各个 Role 的设备配置文件。如果人工地对配置模版中的字符进行逐个替换,工作量太大了,而且也很容易出错。最好的办法就是用配置模版自动化地生成设备配置文件。


如果使用 Python 语言,则可以使用 Jinja2 来帮助我们完成这项工作。


Jinja2 是一个通用的库,应用非常广泛,可以用模版和若干变量的 Key-Value 输出任何文本,当然也包括网络设备的配置文件。


对于配置文件中,需要变化的部分,我们可以把它们设置成变量,用两个大括号 {{  }} 括起来。最好前后都留一个空格,这样看起来清晰美观。所有的变量的值,我们可以用 Python 的字典来存储。


例如,我们要配置一个 SVI,下面是一个简单的例子:


    #!/usr/bin/env python3
    # Example 1
    import jinja2


    my_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 redirects
    no ipv6 redirects
    ip address {{ SVI_IP }}/25
    ip ospf authentication message-digest
    ip ospf message-digest-key 1 md5 {{ OSPF_PWD }}
    ip ospf dead-interval 4
    ip ospf hello-interval 1
    ip 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 Vlan200
        description Route-Access
        no ip redirects
        no ipv6 redirects
        ip address 10.0.0.1/25
        ip ospf authentication message-digest
        ip ospf message-digest-key 1 md5 Password
        ip ospf dead-interval 4
        ip ospf hello-interval 1
        ip ospf priority 255
        ip router ospf 200 area 200
        no 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 2


          import jinja2


          my_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 redirects
          no ipv6 redirects
          ip address {{ SVI_IP }}/25
          ip ospf authentication message-digest
          ip ospf message-digest-key 1 md5 {{ OSPF_PWD }}
          ip ospf dead-interval 4
          ip 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 Vlan200
              description Route-Access
              no ip redirects
              no ipv6 redirects
              ip address 10.0.0.2/25
              ip ospf authentication message-digest
              ip ospf message-digest-key 1 md5 Password
              ip ospf dead-interval 4
              ip ospf hello-interval 1
            ip ospf priority 254
              ip router ospf 200 area 0.0.0.200
              no shutdown


            我们再丰富一下脚本的内容。首先,判断接口是否属于某个 VRF,再判断是否需要启用 IPv6。如果答案为 True,则增加相应的配置。


              #!/usr/bin/env python3
              # Example 3


              import jinja2


              my_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 redirects
              no ipv6 redirects
              ip address {{ SVI_IP }}/25
              ip ospf authentication message-digest
              ip ospf message-digest-key 1 md5 {{ OSPF_PWD }}
              ip ospf dead-interval 4
              ip 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 }}/64
              ipv6 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 1
              ospfv3 dead-interval 4
              ospfv3 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 Vlan200
                  description Route-Access
                  no ip redirects
                  no ipv6 redirects
                  ip address 10.0.0.2/25
                  ip ospf authentication message-digest
                  ip ospf message-digest-key 1 md5 Password
                  ip ospf dead-interval 4
                  ip ospf hello-interval 1
                  ip ospf priority 254
                  ip router ospf 200 area 0.0.0.200
                ipv6 address 2001::1/64
                ipv6 router ospfv3 200 area 0.0.0.200
                ipv6 nd reachable-time 1000
                ospfv3 priority 254
                ospfv3 hello-interval 1
                ospfv3 dead-interval 4
                ospfv3 mtu-ignore
                  no 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 redirects
                    ip address {{ intf_dict.ip_addr }}/{{intf_dict.ip_mask}}
                    {#- 判断是否需要配置 OSPF #}
                    {%- if intf_dict.ospf == True %}
                    ip ospf authentication message-digest
                    ip ospf message-digest-key 1 md5 {{ intf_dict.ospf_pwd }}
                    ip ospf dead-interval 4
                    ip 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 redirects
                    ipv6 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 1
                    ospfv3 dead-interval 4
                    ospfv3 mtu-ignore
                    {%- endif %}
                    {%- endif %}
                    no shutdown
                    {% endfor %}


                    最后,写一个 Python 脚本。这次我们把输出写入一个文本文件而不是直接打印:


                      #!/usr/bin/env python3
                      # Example 4


                      import yaml
                      import jinja2


                      yaml_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 vlan100
                        description VLAN_NAME_100
                        vrf member VRF1
                        no ip redirects
                        ip address 10.0.100.1/25
                        ip ospf authentication message-digest
                        ip ospf message-digest-key 1 md5 Password
                        ip ospf dead-interval 4
                        ip ospf hello-interval 1
                        ip ospf priority 254
                        ip router ospf 100 area 0.0.0.100
                        no ipv6 redirects
                        ipv6 address 2001::1/64
                        ipv6 nd reachable-time 1000
                        ipv6 router 100 area 0.0.0.100
                        ospfv3 priority 254
                        ospfv3 hello-interval 1
                        ospfv3 dead-interval 4
                        ospfv3 mtu-ignore
                        no shutdown


                        interface Vlan200
                        description VLAN_NAME_200
                        no ip redirects
                        ip address 10.0.200.1/25
                        ip ospf authentication message-digest
                        ip ospf message-digest-key 1 md5 Password
                        ip ospf dead-interval 4
                        ip ospf hello-interval 1
                        ip ospf priority 254
                        ip router ospf 200 area 0.0.0.200
                        no shutdown


                        interface Vlan300
                        description VLAN_NAME_300
                        vrf member VRF3
                        no ip redirects
                        ip address 10.0.300.1/25
                        no ipv6 redirects
                        ipv6 address 2003::1/64
                        ipv6 nd reachable-time 1000
                          no shutdown 


                        Done.

                        文章转载自非资深老网工,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                        评论