在平时的网络运维工作中,网工少不了要给新设备做批量配置。其中一些配置比如line vty下的transport input ssh以及AAA、SNMP之类的属于共有配置,所有此类命令在所有设备上是统一没有差异的,写一个Python脚本用Paramiko、Netmiko或者pexpect等SSH模块就能轻松搞定。但是针对那些有差异化的配置,比如每个端口下的description(比如用来描述每个端口下面所连接的服务器的名称,服务器的物理端口(网卡)号,以及服务器的用途等等),每个端口的功能(access port还是trunk port?如果是access port,access哪个VLAN?因为存在差异,你不能简单的用一个interface range来一次性统一完成所有端口的配置)以及在不同交换机上划分和创建不同的VLAN id等等,光靠上述SSH模块是很难搞定的,就算能搞定,写出来的脚本的代码量也是相当庞大,不便于维护的。这时就有必要用到Jinja2这个模板引擎来帮助我们完成这些量大又有差异化的配置。

本文将以一台思科的Nexus 9300交换机为例,讲解如何使用Jinja2来完成对该交换机12个端口的配置(12个端口用来做讲解和演示不多不少)。


1. CSV配置文件及csv模块在Python中的使用

在讲Jinja2之前,首先要知道怎么写配置文件。配置文件可以用YAML,也可以用CSV来写,但是鉴于后者的受众更多(会Excel的肯定并比会YAML的人多,你身边不懂编程的同事和同行更是如此),因此本文的示例配置文件将用CSV来写,并且会简单介绍如何使用Python内置的csv模块来导入我们用Excel写的csv文件的内容。

首先来看下面在Excel里写好的CSV文件,文件名为“switch ports.csv”:




python 导入commands python导入csv_CSV


该CSV文件包含了nexus 9300交换机的12个端口的端口号,其中Server,Link,Purpose下的内容对应每个端口的description配置,用来描述交换机每个端口下面所连接的服务器的名称,服务器的物理端口(网卡)号,以及服务器的用途,具体命令的格式为“description Link to 【Server】 port 【port】 for 【purpose】”,最后的VLAN将用来区别该端口是trunk port还是access port,方便我们配置“switchport mode access|trunk”,以及"switchport access vlan xxx"。

接下来看下如何在Python中使用csv模块来将上面的CSV文件里的内容导入Python中为我们所用(笔者所用Python版本为3.8.2),代码如下:

import csvf = open('switch ports.csv')reader = csv.DictReader(f)for row in reader: print (row)f.close()
  • csv是Python内置模块,无需安装即可导入直接使用。
  • 用open()函数打开上面提到的CSV文件,对其调用csv模块的DictReader()函数,并将它赋值给reader这个变量,该变量是一组特殊的包含“有序的字典”的可迭代对象(字典本身是无序的,但是csv的DictReader()函数返回的可迭代对象里的元素是一组有序的字典)。
  • 用for语句遍历reader里的内容然后将它们按顺序打印出来,最后用close()关闭刚才打开的CSV文件。

执行代码后的效果如下:


python 导入commands python导入csv_python 导入commands_02


可以看到我们通过csv模块打印出了CSV文件下的所有内容,每行的内容格式为有序的字典,方便我们后续引用。

2. Jinja2配置模板

有了CSV写好的配置文件后,接下来我们要用Jinja2来写配置模板了。首先在“switch ports.csv”文件所在的同一目录下新建一个文本文件,用记事本将它打开,写入下面的配置模板内容后,点击记事本左上角的文件(F)-->另存为(A),将文件名设为“interface-template.j2”,将它的“保存类型”改为“所有文件”,最后点击“保存”,这样我们就得到了一个扩展名为'.j2'的Jinja2模板文件了


python 导入commands python导入csv_CSV_03


为了方便读者复制粘贴,下面给出配置模板里的具体内容:

interface {{ interface }}  description Link to {{ server }} port {{ port }} for {{ purpose }}  switchport  {% if vlan == "Trunk" -%}  switchport mode trunk  {% else -%}  switchport mode access  switchport access vlan {{ vlan }}  spanning-tree port type edge  {% endif -%}  no shutdown
  • 在Jinja2中,我们用{{ 变量名 }}的格式来定义变量(注意变量名和左右两边的大括号之间分别隔了一个空格),很显然,这里我们定义的{{ interface }}, {{ server }}, {{ port }}, {{ purpose }},{{ vlan }}分别对应的是CSV配置文件里的A1栏至E1栏里的Interface,Server, Port, Purpose, Vlan。
interface {{ interface }}  description Link to {{ server }} port {{ port }} for {{ purpose }}switchport access vlan {{ vlan }}
  • 在Jinja2中,if、else语句的格式为{% if 条件 -%}和{% else -%},这里要表达的意思是:如果CSV配置文件里的Vlan(E栏)下对应的内容为Trunk,那么我们将该端口配置为trunk port,如果端口模式为非Trunk,那我们将端口配置为access port,并分配对应的vlan给它,最后将其生成树的端口模式改为edge。注意Jinja2中的if语句最后需要用{% endif -%}来收尾。
{% if vlan == "Trunk" -%}  switchport mode trunk  {% else -%}  switchport mode access  switchport access vlan {{ vlan }}  spanning-tree port type edge  {% endif -%}
  • 最后不管端口类型是trunk port还是access port,我们都用no shutdown命令开启它(因为no shutdown写在{% endif -%}下面):
no shutdown

这里介绍了如何使用Jinja2来创建配置模板以及Jinja2的部分语法,下面我们来看下如何在Python中使用Jinja2。

3. Jinja2在Python中的使用

前面讲到了我们在Jinja2的配置模板里定义了{{ interface }}, {{ server }}, {{ port }}, {{ purpose }},{{ vlan }}五个变量,来分别对应CSV配置文件里的A1栏至E1栏里的Interface,Server, Port, Purpose, Vlan。接下来要做的是将两者真正结合,生成最后我们需要的Nexus 9300交换机的的实际配置命令,这里我们需要借助Python来完成,代码如下:

from jinja2 import Templateimport csvcsv_file = open('switch ports.csv')template_file = open('interface-template.j2')reader = csv.DictReader(csv_file)interface_template = Template(template_file.read(), keep_trailing_newline=True)interface_configs = ''for row in reader:    interface_config = interface_template.render(    interface = row['Interface'],    vlan = row['VLAN'],    server = row['Server'],    port = row['Port'],    purpose = row['Purpose']    )    interface_configs += interface_configprint(interface_configs)
  • Jinja2从Python2.4起被引入为Python的内置模块,无需安装即可import导入使用,其中Template是jinja2模块下最常用的类,这里我们用from jinja2 import Template将其导入。随后我们用open()函数打开之前创建好的“switch ports.csv”和“interface-template.j2”两个文件,并分别赋值给csv_file和template_file两个变量。
from jinja2 import Templateimport csvcsv_file = open('switch ports.csv')template_file = open('interface-template.j2')
  • "reader = csv.DictReader(csv_file)"的作用在前面已经讲到了,不再赘述。这里我们用jinja2的Template()函数将j2配置模板文件模板化,并将它赋值给interface_template这个变量,因为这个模板要被反复使用12次(我们有12个交换机端口需要配置),Template()函数里的“keep_trailing_newline=True”这个参数的作用是在每使用完一次模板后,自动替我们在配置命令的最后加入一个换行符,避免下面这种命令格式问题的出现:


python 导入commands python导入csv_网络工程师的python之路 pdf_04


reader = csv.DictReader(csv_file)interface_template = Template(template_file.read(), keep_trailing_newline=True)
  • 这里创建一个空的字符串,将其赋值给interface_configs这个变量,它的作用下面马上会讲到。
interface_configs = ''
  • 前面讲到了csv的DictReader()函数返回的是可迭代的对象,这个可迭代对象里的元素是一组一组有序的字典,这里我们使用for语句来遍历reader这个DictReader()函数返回的可迭代对象,并调用Template()下的render()函数对配置模板做渲染,这样配置文件和配置模板就被结合起来了。每调用一次配置文件和配置模板后,我们就能生成一个接口下的配置命令(从Eth1/1开始直到Eth1/12),每个接口配置命令的数据格式都为字符串。最后我们用"interface_configs += interface_config"将12个接口的配置(字符串)全部汇总进上面创建的interface_configs这个空字符串变量,然后通过print (interface_configs)来验证其内容。
interface_configs = ''for row in reader:    interface_config = interface_template.render(    interface = row['Interface'],    vlan = row['VLAN'],    server = row['Server'],    port = row['Port'],    purpose = row['Purpose']    )    interface_configs += interface_configprint(interface_configs)

执行代码后的效果如下:


python 导入commands python导入csv_网络工程师的python之路 pdf_05


可以看到运行脚本后,代码将jinja2配置模板渲染成了我们实际需要的12个端口的配置命令。

4. 将生成的配置命令上传给交换机并执行

用Jinja2配置模板配合CSV配置文件生成了我们想要的实际配置命令后,最后一步自然是将该配置命令上传给交换机并执行,这里我们使用netmiko来完成, 在上面的代码中添加一些内容,如下:

from jinja2 import Templateimport csvfrom netmiko import ConnectHandlercsv_file = open('switch ports.csv')template_file = open('interface-template.j2')reader = csv.DictReader(csv_file)interface_template = Template(template_file.read(), keep_trailing_newline=True)interface_configs = ''for row in reader:    interface_config = interface_template.render(    interface = row['Interface'],    vlan = row['VLAN'],    server = row['Server'],    link = row['Port'],    purpose = row['Purpose']    )    interface_configs += interface_configconfig_set = interface_configs.split('')SW = { 'device_type': 'cisco_nxos', 'ip': '10.1.1.1', 'username': 'admin', 'password': 'pass',    }connect = ConnectHandler(**SW)print ('Connected to switch')output = connect.send_config_set(config_set, cmd_verify=False)print(output)

Netmiko没什么好讲的,只需要注意两点:

  • 1. 这里我们用"config_set = interface_configs.split('')"将字符串的配置命令用split('')隔行拆解成一个列表,方便我们配合Netmiko的send_config_set()函数来对交换机做配置。
  • 2. 知道"output = connect.send_config_set(config_set, cmd_verify=False)"中的"cmd_verify=False"这个参数意义的人不多,这里讲下它的作用:同Netmiko 2不一样,Netmiko 3中默认要等到输入的命令在屏幕上打印出来后才会执行后面的命令(因为Netmiko 3默认将send_config_set()里的"cmd_verify"参数设为True),像我们这种一次性对交换机输入多达60多条命令的情况(12个端口要配置),经常会出现网络延迟的问题导致在执行脚本时Netmiko会返回“netmiko.sshexception.NemikoTimeoutException:Time-out reading channel, data not available”这个异常(我们写的代码本身没有问题,这是Netmiko 3自身的一个"bug"),如下:


python 导入commands python导入csv_CSV_06


我使用的是Netmiko 3.1.1版本,因此必须将"cmd_verify"参数设为False,写成"output = connect.send_config_set(config_set, cmd_verify=False)",如果你使用的是Netmiko 2则没有这个顾虑(可以通过pip freeze来查看你的Netmiko版本)。

最后执行脚本前,我们先验证Nexus 9300交换机上现有的配置,这里我们选取Eth1/1, Eth1/2, Eth1/9, Eth1/11查看,可以发现目前这些端口下没有任何配置,并且"show interface description"也可以看到前面12个端口没有任何description配置(除了Eth1/5当前有一个“L3 LINK”的description,但是不影响我们的配置和验证)


python 导入commands python导入csv_python 导入commands_07


python 导入commands python导入csv_python 导入commands_08


执行脚本看效果:


python 导入commands python导入csv_csv导入access_09


配置完成后再次验证交换机的端口配置,并用"show interface description"进一步验证:


python 导入commands python导入csv_网络工程师的python之路 pdf_10


python 导入commands python导入csv_sklearn导入csv文件_11