作为Paramiko最为成功的衍生模块,Netmiko成为了很多学习Python网络运维自动化技术的网工日常工作中最常用的模块之一。相较于Paramiko,Netmiko将很多细节优化和简化,比如不需要导入time模块做休眠,输入每条命令不需要在后面加换行符 ,不需要执行config term,exit,end等命令,提取、打印回显内容更方便,可以配合Jinja2模块调用配置模板,以及配合TextFSM、pyATS、Genie等模块将回显内容以有序的JSON格式输出,方便我们过滤和提取出所需的数据等等,并且在Netmiko的基础上也诞生出了napalm, pyntc,netdev等扩展模块甚至Nornir这样成功的网络运维自动化框架。
在我的《网络工程师的Python之路》一书里,我刻意减少了Netmiko的相关内容,重点讲解了Paramiko,因为道理很简单:Netmiko将太多功能简化的做法其实并不利于初学者学习。Netmiko和Paramiko两者就像自动挡汽车和手动挡汽车的区别。会驾车的都知道,一开始就学手动挡车的人100%会开自动挡的车,而从一开始就学自动挡车的人,除非额外加课,否则是100%不会开手动挡的车的。Paramiko虽然复杂、繁琐一些,但是就像手动挡车一样,整体“操控感”更强,运维脚本中的所有细节和各种参数都在我们自己的掌控之中,更利于我们从整体来把握进而写出自己需要的脚本,并且无需像Netmiko那样担心对各种设备各种OS的支持的问题。
随着越来越多的网工读者们逐渐上手和适应Paramiko,为了弥补我书里“重Paramiko,轻Netmiko”的遗憾,特此补上一篇《Netmiko终极指南》,本文和之前我连载的Nornir3.0.0的教程类似,将Netmiko的各个知识点直接以实验形式演示、讲解,总计8个实验,由简到难涵盖Netmiko的各个知识点。
实验平台:
本文承接《网络工程师的Python之路》过往的拓扑,还是老规矩:一台CentOS 8主机(192.168.2.1)上跑Netmiko,下面连上5台试验用的虚拟三层思科交换机IP从192.168.2.11到192.168.2.15。
实验准备:
通过pip安装netmiko后在Python解释器里用import验证,截至2021年4月27号,目前最新的Netmiko版本为3.4.0。
要通过netmiko来登录一台设备需要用到它的核心对象ConnectHandler。ConnetHandler()包含几个必要的参数和可选参数,必要参数包括device_type,ip(也可以为host),username和password,可选参数包括port, secret,use_keys,key_file,conn_timeout等等,可选参数的作用和用法在后面的实验中会陆续讲到。
首先创建第一个脚本netmiko1.py,然后写入下面代码:
from netmiko import ConnectHandler
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**sw1) as connect:
print ("已经成功登陆交换机" + sw1['ip'])
代码分段讲解:
from netmiko import ConnectHandler
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**sw1) as connect:
print ("已经成功登陆交换机" + sw1['ip'])
from netmiko import ConnectHandler
with ConnectHandler(device_type='cisco_ios', ip='192.168.2.11', username='python', password='123') as connect:
print ("已经成功登陆交换机192.168.2.11")
运行脚本看效果:
运行脚本前先在S1上开启debug ip ssh,以便我们验证脚本是否真正SSH登录了交换机。
运行脚本:
然后回到S1上,如果看到如下debug日志,则说明netmiko登录交换机成功:
Netmiko主要有四种函数向设备做配置:send_command(),send_config_set()以及send_config_from_file(),除此之外还有一个不太常用的send_command_timing(),它们的用法和区别如下:
send_command():只支持向设备发送一条命令,通常是show/display之类的查询、排错命令或者wr mem这样保存配置的命令。发出命令后,默认情况下这个函数会一直等待,直到接收到设备的完整回显内容为止(以收到设备提示符为准,比如说要一直等到读取到“S1#"为止),如果想要指定netmiko从回显内容中读到我们需要的内容,则需要用到expect_string参数(expect_string默认值为None),如果send_command()从回显内容中读到了expect_string参数指定的内容,则send_command()依然返回完整的回显内容,如果没读到expect_string参数指定的内容,则netmiko会返回一个OSError: Search pattern never detected in send_command: xxxxx的异常,关于expect_string参数的用法会在稍后的实验里演示。
send_config_set(): 向设备发送一条或多条配置命令,注意是配置命令,不是show/display之类的查询命令,因为send_config_set()本身会自动替我们加上一个config terminal命令进入配置模式(以及在命令末尾自动替我们加上一个end命令),在config terminal下除非在show命令前面加上一个do,比如do show ip int brief,否则show命令无效(以上以思科IOS设备为例)。send_config_set()一般配合列表使用。
send_config_from_file(): 在配置命令数量较多的时候,将所有配置命令写入列表显然是比较笨拙的,因为会造成代码过长,不方便阅读,并且在部分厂商的设备上(比如华为)还会出现超时报错的情况。我们可以先将所有的配置命令写入一个配置文件中,然后使用send_config_from_file()去读取该文件的内容帮助我们完成配置。和send_config_set()一样,send_config_from_file()也会自动帮我们添加config terminal和end两个命令,所以在我们的配置文件里无需加入这两个命令。
send_command_timing(): 和send_command()一样,只支持向设备发送一条show/display命令。区别是在用send_command()输入一条show命令后,send_command()会一直等待,直到接收到设备的完整回显内容为止(以收到设备提示符为准)。send_command_timing()则会自己去“猜”什么时候停止运行,它的原理是如果没有从设备收到更多新的回显内容后,它会继续等待2秒钟,然后才停止运行,send_command_timing()里有一个叫做delay_factor的参数,默认为1,如果将它修改为2,则send_command_timing()会等待4秒钟,修改为3,则等待6秒,修改为4,等待8秒。。以此类推。有时候在输入show tech-support或者show log(logging buffer特别大,日志特别长的那种)这种回显内容巨多的命令时,即使输入了term len 0来取消分屏显示,设备在返回回显内容时依然会有停顿,有时停顿时间会大于2秒钟,这样就会导致截屏不完整,必须手动去调整delay_factor这个参数。
接下来做实验,首先创建一个config.txt文件,然后写入下列配置命令,这里我们将gi0/0端口的description改为Netmiko2.py
int gi0/0
description Netmiko2.py
然后创建第二个实验脚本netmiko.py2(确保实验脚本和config.txt配置命令位于同一个文件夹下),在脚本中放入下列代码:
from netmiko import ConnectHandler
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
commands = ['interface gi0/1', 'description Nornir2.py']
with ConnectHandler(**sw1) as connect:
print ("已经成功登陆交换机" + sw1['ip'])
output = connect.send_command('show interface description')
print(output)
output = connect.send_config_set(commands)
print(output)
output = connect.send_config_from_file('config.txt')
print(output)
output = connect.send_command('show interface description')
print(output)
output = connect.send_command('wr mem')
print(output)
代码分段讲解:
from netmiko import ConnectHandler
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
commands = ['interface gi0/1', 'description Nornir2.py']
with ConnectHandler(**sw1) as connect:
print ("已经成功登陆交换机" + sw1['ip'])
output = connect.send_command('show interface description')
print(output)
output = connect.send_config_set(commands)
print(output)
output = connect.send_config_from_file('config.txt')
print(output)
output = connect.send_command('show interface description')
print(output)
output = connect.send_command('wr mem')
print(output)
运行脚本看效果:
在我的书中曾重点介绍过什么是Textfsm以及它的用法。除了Textfsm之外,思科也出过类似的工具pyATS和Genie来帮助我们将网络设备无序的回显内容以有序的JSON格式输出(不过只限于思科设备)。在Netmiko的send_command()和send_command_timing()中已经内置了可以直接调用Textfsm和Genie的参数,非常方便。实验3就来分别介绍一下它们的用法:
首先通过pip install来下载TextFSM,pyATS和Genie,这里要注意的是pyATS和Genie只支持Linux不支持Windows(Windows下运行pip install pyats genie会报错),如果你是Windows用户的话需要使用WLS2或者通过虚拟机运行Linux再来安装pyATS和Genie。另外并且强烈建议先通过pip install --upgrade pip将pip升级到最新版本,否则下载的pyATS和Genie会碰到各种奇怪的问题。
pip install --upgrade pip
pip install textfsm
pip install pyats
pip install genie
我们以show interfaces命令为例,首先创建一个test.py来看下不使用textfsm或genie,输入该命令后的回显内容:
test.py代码如下:
from netmiko import ConnectHandler
import pprint
connection_info = {
'device_type': 'cisco_ios',
'host': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**connection_info) as conn:
out = conn.send_command("show interfaces")
print(out)
运行脚本后的效果:
接下来创建脚本netmiko3_1.py,写入下面代码:
from netmiko import ConnectHandler
connection_info = {
'device_type': 'cisco_ios',
'host': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**connection_info) as conn:
out = conn.send_command("show interfaces", use_textfsm=True)
print(out)
可以看到和Nornir一样,要在Netmiko里调用textfsm很简单,只需要在send_command()或者send_command_timing()里直接添加一个参数use_textfsm=True即可(默认为False)。接下来运行脚本看效果:
这里可以看到虽然textfsm输出的内容为JSON格式,但是依然不具备可读性。为了将内容更美观地打印出来,这里我们可以用到Python另外一个很强大的内置库:pprint,pprint全称是pretty printer,它的功能是将各种数据结构更美观地输出。这里我们将netmiko3_1.py的代码稍作修改,在第二行加上from pprint import pprint,在最后一行将print(out)改为pprint(out)即可,如下:
from netmiko import ConnectHandler
from pprint import pprint
connection_info = {
'device_type': 'cisco_ios',
'host': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**connection_info) as conn:
out = conn.send_command("show interfaces", use_textfsm=True)
pprint(out)
运行代码看效果,输出内容是不是更美观,可读性更强了?
和textfsm一样,在Netmiko中使用genie也很简单,只需要在send_command()或者send_command_timing()里直接添加一个参数use_genie=True即可(默认为False),当然前提是你之前有通过pip安装了pyATS和Genie(虽然参数只是use_genie,但是pyATS也必须下载安装,缺一不可),接下来我们再创建netmiko3_2.py,看下用genie配合pprint将数据内容输出后是什么效果,代码如下:
from netmiko import ConnectHandler
from pprint import pprint
connection_info = {
'device_type': 'cisco_ios',
'host': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**connection_info) as conn:
out = conn.send_command("show interfaces", use_genie=True)
pprint(out)
运行代码看效果,可以看到Netmiko使用genie配合pprint后输出的内容比Textfsm+pprint更美观,层次更分明:
那么我们可以参照paramiko的方法,创建一个ip_list.txt文件,将所有设备的管理IP地址都写进去,如下,这里我将SW1-SW5总共5个交换机的IP地址都写入了ip_list.txt文件里:
from netmiko import ConnectHandler
import pprint
connection_info = {
'device_type': 'cisco_ios',
'host': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**connection_info) as conn:
out = conn.send_command("show interfaces", use_genie=True)
for name, details in out.items():
print(f"{name}")
print(f"- Status: {details.get('enabled', None)}")
print(f"- Physical address: {details.get('phys_address', None)}")
print(f"- Duplex mode: {details.get('duplex_mode', None)}")
for counter, count in details.get('counters', {}).items():
if isinstance(count, int):
if count > 0:
print(f"- {counter}: {count}")
elif isinstance(count, dict):
for sub_counter, sub_count in count.items():
if sub_count > 0:
print(f"- {counter}::{sub_counter}: {sub_count}")
脚本运行后的效果如下:
前面三个实验我们都是通过Netmiko登录一台设备,实验4里我们来看下怎么通过Netmiko登录多台设备,根据情况不同主要有两种方法,分两个脚本讲解。
第一种情况:生产网络里所有设备的username, password, port这些参数都一样
那么我们可以参照paramiko的方法,创建一个ip_list.txt文件,将所有设备的管理IP地址都写进去,如下,这里我将SW1-SW5总共5个交换机的IP地址都写入了ip_list.txt文件里:
然后创建脚本netmiko4_1.py,写入下列代码:
from netmiko import ConnectHandler
with open('ip_list.txt') as f:
for ips in f.readlines():
ip = ips.strip()
connection_info = {
'device_type': 'cisco_ios',
'ip': ip,
'username': 'python',
'password': '123',
}
with ConnectHandler(**connection_info) as conn:
print (f'已经成功登陆交换机{ip}')
output = conn.send_command('show run | i hostname')
print(output)
代码分段讲解:
from netmiko import ConnectHandler
with open('ip_list.txt') as f:
for ips in f.readlines():
ip = ips.strip()
connection_info = {
'device_type': 'cisco_ios',
'ip': ip,
'username': 'python',
'password': '123',
}
with ConnectHandler(**connection_info) as conn:
print (f'已经成功登陆交换机{ip}')
output = conn.send_command('show run | i hostname')
print(output)
运行脚本看效果:
第二种情况:生产网络里设备的username, password, port这些参数不尽相同
这种情况下就不能再用ip_list.txt的方法来做了,必须给每一个参数不同的交换机创建一个字典,因为每个字典一般都要占据5,6排代码的空间,设备一多的时候脚本代码量会非常恐怖。我们可以将这些字典写入一个额外的文件类型为json的文件,将其取名为switches.json,在Python脚本里可以导入Python内置的json模块,利用json.load()这个函数来加载switches.json文件里的内容,因为json.load()会返回一个列表类型的JSON数据,我们可以使用for循环来遍历该列表里的JSON数据来达到依次登录多台设备的目的。具体方法如下:
首先创建switches.json文件,将包含交换机S1和S2的数据以字典形式写入:
[
{
"name": "SW1",
"connection": {
"device_type": "cisco_ios",
"host": "192.168.2.11",
"username": "python",
"password": "123"
}
},
{
"name": "SW2",
"connection": {
"device_type": "cisco_ios",
"host": "192.168.2.12",
"username": "python",
"password": "123"
}
}
]
然后创建实验4的第二个脚本netmiko4_2.py
import json
from netmiko import ConnectHandler
with open("switches.json") as f:
devices = json.load(f)
for device in devices:
with ConnectHandler(**device['connection']) as conn:
hostname = device['name']
print (f'已经成功登陆交换机{hostname}')
output = conn.send_command('show run | i hostname')
print(output)
代码分段讲解:
import json
from netmiko import ConnectHandler
with open("switches.json") as f:
devices = json.load(f)
for device in devices:
with ConnectHandler(**device['connection']) as conn:
hostname = device['name']
print (f'已经成功登陆交换机{hostname}')
output = conn.send_command('show run | i hostname')
print(output)
运行脚本看效果:
Jinja2是Python的内置模块,无需通过pip下载安装。Jinja2在我的专栏里已经多次提到过,这里就不介绍它的基本语法和用法了,这里直接给出它在Netmiko中的用法。
这里我们将使用netmiko配合jinja2给SW1的loopback 1端口配置一个inbound方向的ACL 1,该ACL 1会分别允许和拒绝一些IP地址。首先创建一个文件夹,取名templates,进入该文件夹后创建我们的jinja2模板文件,将其取名为acl.conf.tpl,然后放入下面的jinja2语句:
interface {{ interface }}
ip access-group 1 in
{% for host in disallow_ip %}
access-list 1 deny host {{ host }}
{% endfor %}
{% for host in allow_ip %}
access-list 1 permit host {{ host }}
{% endfor %}
from netmiko import ConnectHandler
from jinja2 import Environment, FileSystemLoader
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
loader = FileSystemLoader('templates')
environment = Environment(loader=loader)
tpl = environment.get_template('acl.conf.tpl')
allow_ip = ['10.1.1.1', '10.1.1.2']
disallow_ip = ['10.1.1.3', '10.1.1.4']
out = tpl.render(allow_ip=allow_ip, disallow_ip=disallow_ip, interface='loopback 1')
with open("configuration.conf", "w") as f:
f.write(out)
with ConnectHandler(**sw1) as conn:
print ("已经成功登陆交换机" + sw1['ip'])
output = conn.send_config_from_file("configuration.conf")
print (output)
from netmiko import ConnectHandler
from jinja2 import Environment, FileSystemLoader
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
loader = FileSystemLoader('templates')
environment = Environment(loader=loader)
tpl = environment.get_template('acl.conf.tpl')
allow_ip = ['10.1.1.1', '10.1.1.2']
disallow_ip = ['10.1.1.3', '10.1.1.4']
out = tpl.render(allow_ip=allow_ip, disallow_ip=disallow_ip, interface='loopback 1')
with open("configuration.conf", "w") as f:
f.write(out)
with ConnectHandler(**sw1) as conn:
out = conn.send_config_from_file("configuration.conf")
运行脚本看效果:
运行脚本前首先确认SW1里没有ACL 1,并且loopback 1下也没有放入任何ACL:
脚本运行完毕后再次返回SW1验证:
在我之前的所有专栏文章以及书里我都是将SSH用户名的特权级别设为15,跳过了输入enable密码这一步。鉴于部分读者的生产网络中仍然要求使用enable密码来进入设备的特权模式,这里就来看下如何在Netmiko中使用enable密码。
开始实验之前,首先将SW1上的用户python的特权级别从之前的15改为1:
然后使用用户名python手动SSH登录SW1,可以发现输入SSH密码后,来到了用户模式(>提示符),必须再次输入enable密码才能进入特权模式:
验证完毕后,开始创建实验6的脚本netmiko6.py,写入下列代码:
from netmiko import ConnectHandler
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123',
'secret': '123'
}
with ConnectHandler(**sw1) as connect:
connect.enable()
print ("已经成功登陆交换机" + sw1['ip'])
output = connect.send_command('show run')
print (output)
代码分段讲解:
from netmiko import ConnectHandler
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123',
'secret': '123'
}
with ConnectHandler(**sw1) as connect:
connect.enable()
print ("已经成功登陆交换机" + sw1['ip'])
output = connect.send_command('show run')
print (output)
运行脚本看效果:
开始实验7之前不要忘记先把用户python的特权级别从1改回15。
平常的网络运维工作中少不了要向设备传送文件,比如新的OS的镜像文件。实验7将演示如何使用Netmiko配合scp协议来向设备(SW1)传送文件。
首先在SW1上开启scp:
然后创建一个名为test.txt的文本文件,在其中写入test这个单词。
然后创建脚本netmiko7.py,写入下列代码:
from netmiko import ConnectHandler, file_transfer
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**sw1) as connect:
print ("已经成功登陆交换机" + sw1['ip'])
output = file_transfer(connect,
source_file="test.txt",
dest_file="test.txt",
file_system="flash:",
direction="put")
print (output)
代码分段讲解:
from netmiko import ConnectHandler, file_transfer
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**sw1) as connect:
print ("已经成功登陆交换机" + sw1['ip'])
output = file_transfer(connect,
source_file="test.txt",
dest_file="test.txt",
file_system="flash:",
direction="put")
print (output)
运行脚本看效果:
首先查看SW1的文件系统flash0,确认里面没有test.txt文件
运行脚本:
注意这里print(output)返回的内容为一个字典,如果文件传输失败的话,该字典的file_transferred这个键的值将为False。
最后回到SW1上验证,可以看到test.txt文件已经成功上传到SW1的flash0::
在网络设备中输入某些命令后系统会返回一个提示命令,询问你是继续执行命令还是撤销命令,比如我们在实验7中向SW1传入了test.txt这个文件,如果这时我们想将它删除,则需要输入命令del flash0:/test.txt,输入该命令后,系统会询问你是否confirm,如下:
这时如果你输入字母y或者敲下回车键则系统会执行del命令将该文件删除,如果你输入字母n,则系统会中断该命令,该文件不会被删除。
在Netmiko中我们可以调用send_command()函数中的command_string,expect_string,strip_prompt,strip_command四个参数来应对这种情况。
首先创建实验8的脚本netmiko8.py,写入下列代码:
from netmiko import ConnectHandler
sw1 = {
'device_type': 'cisco_ios',
'ip': '192.168.2.11',
'username': 'python',
'password': '123'
}
with ConnectHandler(**sw1) as connect:
print ("已经成功登陆交换机" + sw1['ip'])
output = connect.send_command(command_string="del flash0:/test.txt",
expect_string=r"Delete flash0:/test.txt?",
strip_prompt=False,
strip_command=False)
output += connect.send_command(command_string="y",
expect_string=r"#",
strip_prompt=False,
strip_command=False)
print(output)
output = connect.send_command(command_string="del flash0:/test.txt",
代码分段讲解:
from netmiko import ConnectHandler
sw1 ={
'device_type':'cisco_ios',
'ip':'192.168.2.11',
'username':'python',
'password':'123'
}
from netmiko import ConnectHandler
with ConnectHandler(**sw1) as connect:
print ("已经成功登陆交换机" + sw1['ip'])
output = connect.send_command(command_string="del flash0:/test.txt",
expect_string=r"Delete flash0:/test.txt?",
strip_prompt=False,
strip_command=False)
output += connect.send_command(command_string="y",
expect_string=r"#",
strip_prompt=False,
strip_command=False)
print(output)
output = connect.send_command(command_string="del flash0:/test.txt",
运行脚本看效果:
首先确认目前 SW1的flash0下还有test.txt文件:
运行脚本netmiko8.py:
可以看到Netmiko在遇到提示命令“Delete flash0:/test.txt?”帮我们自动输入了命令y,删除了test.txt文件。
回到SW1上验证,test.txt已被删除:
前面说了,“strip_prompt和strip_command两个参数这里放Fasle就行,目的是让代码最后的print(output)输出的内容的格式更好看一点”, 如果把它们改成True会怎么样呢?来看效果:
显然不如放False的时候美观,并且看不到Netmiko帮我们输入的y这个命令。
个命令。
代码最后的print(output)输出的内容的格式更好看一点”, 如果把它们改成True会怎么样呢?来看效果:
显然不如放False的时候美观,并且看不到Netmiko帮我们输入的y这个命令。
个命令。
果:
显然不如放False的时候美观,并且看不到Netmiko帮我们输入的y这个命令。
个命令。