测试Paramiko配置交换机
-
拓扑
在SW1上配置SSH Server、SSH登录用户和密码
-
创建密钥对
[SW1]rsa local-key-pair create The key name will be: SW1_Host The range of public key size is (512 ~ 2048). NOTES: If the key modulus is greater than 512, it will take a few minutes. Input the bits in the modulus[default = 512]:2048 Generating keys... ..........++++++++++++ ..........++++++++++++ .++++++++ ....++++++++由于ubuntu 20.04是比较新的版本,不再支持1023bit以下长度的密钥,所以直接配置1024或者2048长度的密钥
-
使能Stelnet服务器
stelnet server enable -
配置SSH
-
配置SSH用户登录界面
user-interface vty 0 4 authentication-mode aaa user privilege level 15 protocol inbound ssh # -
配置SSH用户
ssh user sht ssh user sht authentication-type password ssh user sht service-type stelnet #如果SSH用户使用password认证(
authentication-type = password | rsa | password-rsa),则只需要在SSH服务器端生成本地RSA密钥。如果SSH用户使用RSA认证,则在服务器端和客户端都需要生成本地RSA密钥对,并且服务器端和客户端都需要将对方的公钥配置到本地。 -
配置对SSH用户进行password认证
aaa local-user sht password cipher Huawei local-user sht privilege level 15 local-user sht service-type ssh #
-
-
配置管理VLAN
-
创建管理VLAN100
# vlan 100 management-vlan # interface Vlanif100 ip address 192.168.50.1 255.255.255.0 # -
配置Hybrid接口(关于为什么配置Hybrid口,可以参考网络基础回顾[1])
# interface Ethernet0/0/1 port hybrid pvid vlan 100 port hybrid untagged vlan 100 #
-
Ubuntu 20.04用SSH远程登录SW1,验证SW1的SSH配置是否生效
-
在ubuntu 20.04上使用
ssh sht@192.168.50.1,出现报错:$ ssh sht@192.168.50.1 **Unable to negotiate with** 192.168.50.1 port 22: **no matching key exchange method found.** **Their offer: diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1**-
问题描述:
如果客户端和服务器无法就一组共同的参数达成一致,那么连接将失败。OpenSSH (7.0及以上版本)会产生类似这样的错误信息,Ubuntu20.04使用OpenSSH 8.2。在这种情况下,客户端和服务器无法就密钥交换算法达成一致,服务器只提供(即该实验中的SW1)两个方法diffie-hellman-group1-sha1和diffie-hellman-group-exchange-sha1,OpenSSH支持这两种密钥交换方法,但是默认不启用,因为这两种方法版本较久,比较弱(weak),在所谓的Logjam攻击的理论范围内。所以我们需要在客户端启用较弱的方法。
$ ssh -V OpenSSH_8.2p1 Ubuntu-4ubuntu0.1, OpenSSL 1.1.1f 31 Mar 2020 -
解决方案:
来源:
$ ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 sht@192.168.50.1+— 在客户端默认集合中增加该交换算法而不是替换默认算法-o— 选项-KexAlgorithms— 密钥交换方法-用来生成每个连接的密钥;
-
-
使用
ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 sht@192.168.50.1后,成功连接上SW1$ ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 sht@192.168.50.1 The authenticity of host '192.168.50.1 (192.168.50.1)' can't be established. RSA key fingerprint is SHA256:BqJmFBxfyFuobgWlC7MmLlpjfu1UjsyhnLbdd/GIVc8. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '192.168.50.1' (RSA) to the list of known hosts. sht@192.168.50.1's password: Info: The max number of VTY users is 5, and the number of current VTY users on line is 1. The current login time is 2021-03-02 19:12:58. <SW1>sys Enter system view, return user view with Ctrl+Z. [SW1]
配置paramiko_test.py对SW1进行配置
-
编写脚本paramiko_test.py
import paramiko import time ip_address = "192.168.50.1" username = "sht" password = "Huawei" ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # not recommended for production but fine for labs ssh_client.connect(hostname = ip_address, username = username, password = password) print("Successful connection", ip_address) remote_connection = ssh_client.invoke_shell() remote_connection.send("system\n") # creation of vlans for n in range (2,21): print("Creating VLAN " + str(n)) remote_connection.send("vlan " + str(n) + "\n") remote_connection.send("description VLAN " + str(n) + "\n") time.sleep(0.5) # wait half of second remote_connection.send("return\n") time.sleep(1) output = remote_connection.recv(65535) print(output.decode('ascii')) ssh_client.closeparamiko_test.py脚本解释-
第一部分
ip_address = "192.168.50.1" username = "sht" password = "Huawei" ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # not recommended for production but fine for labs ssh_client.connect(hostname = ip_address, username = username, password = password)-
Paramiko官方文档给出的
paramiko.client.SSHClient类的描述为:与SSH服务器建立SSH会话的高级表示,该类封装了Transport,Channel和SFTPClient类来处理好认证和打开通道(channel)。
SSHClient的典型使用方式为:client = SSHClient() client.load_system_host_keys() client.connect('ssh.example.com') # stdin, stdout, stderr = client.exec_command('ls -l') 暂时不用了解使用
SSHClient()创建SSH客户端,上传脚本的一端作为服务端,然后调用load_system_host_keys(filename = None)读取主机密钥,参数filename默认为None,则会尝试从用户的本地 "known hosts"文件中读取密钥;再调用connect()与hostname为ssh.example.com的服务器建立连接,服务器的主机密钥会与系统主机密钥 (load_system_host_keys) 和任何本地主机密钥 (load_host_keys) 对照检查,如果在这两组主机密钥中都找不到服务器的主机名,则使用缺失主机密钥策略 (set_missing_host_key_policy),默认策略是决绝连接并报错SSHException; -
set_missing_host_key_policy(paramiko.AutoAddPolicy( ))set_missing_host_key_policy(policy)设置连接到未知主机密钥的服务器时要使用的策略;policy即要设置的策略类,可以设置为RejectPolicy(默认)、AutoAddPolicy和WarningPolicy等;如果不配置该策略,默认是拒绝与服务器连接的。- 将
paramiko.AutoAddPolicy()设为参数,用于自动将主机名和新的主机密钥添加到本地HostKeys对象中,并将该对信息保存,该方法由SSHClient调用; - 不仅接受了来自未知主机密钥的服务器提供的公钥,建立了连接,还会将主机名与对应的密钥保存到HostKeys对象中,后面再与该服务器进行连接,就可以使用
load_system_host_keys或load_host_keys方法了
-
-
第二部分
remote_connection = ssh_client.invoke_shell() remote_connection.send("system\n") # creation of vlans for n in range (2,21): print("Creating VLAN " + str(n)) remote_connection.send("vlan " + str(n) + "\n") remote_connection.send("description VLAN " + str(n) + "\n") time.sleep(0.5) # wait half of second remote_connection.send("return\n")remote_connection = ssh_client.invoke_shell()- 由于SSHClient类封装了Channel类,类似socket,使用
invoke_shell()后,客户端会在SSH服务器上启动一个交互式shell会话,即用通道类(Channel)新建通道(channel),并按要求的终端类型和大小连接到一个伪终端 ssh_client是SSHClient类的对象,调用SSHClient的方法invoke_shell()后,创建了remote_connection这个通道类(Channel)的通道
- 由于SSHClient类封装了Channel类,类似socket,使用
remote_connection.send()- 通道(Channel)类中的
send()用于传输数据 - 这里remote_connection是通道(channel),调用
send()方法向SSH服务器发送数据
- 通道(Channel)类中的
-
第三部分
time.sleep(1) output = remote_connection.recv(65535) print(output.decode('ascii')) ssh_client.close-
time.sleep(1) -
output = remote_connection.recv(65535)中,remote_connection通道使用recv(65535)设置读取Bytes的最大值;recv()如果返回字节的长度为0时,通道流就会被关闭; -
output.decode('ascii'),recv()的返回值为字节型字符串,将返回值赋值给output变量,然后在Python 3中使用decode('ascii')将字节型字符串解析为ascii编码,帮助输出美观的内容;如果不适用
decode('ascii'),输出如下: -
ssh_client.close关闭ssh_client;close直接关闭SSHClient类
-
-
将配置推送到(pull to)设备
-
上传到SW1【运行代码】
-
在SW1上检查脚本配置是否生效
脚本成功远程配置SW1上的vlans。
配置MSTP场景
拓扑规划与设备基础配置
-
拓扑与规划
- 在MST1中,SwitchA作为根,阻塞SwitchD的Ethernet0/0/2接口
- 在MST2中,SwitchB作为根,阻塞SwitchC的Ethernet0/0/2接口
-
交换机基础配置
rsa local-key-pair create **2048** # stelnet server enable # user-interface vty 0 4 authentication-mode aaa user privilege level 15 protocol inbound ssh # aaa local-user sht password cipher Huawei local-user sht privilege level 15 local-user sht service-type ssh # ssh user sht ssh user sht authentication-type password ssh user sht service-type stelnet # vlan 100 management-vlan # interface Vlanif100 ip address 192.168.50.x 255.255.255.0-
SwitchA | SwitchB
# interface Ethernet0/0/1 port link-type trunk port trunk allow-pass vlan 100 # interface Ethernet0/0/2 port link-type trunk port trunk allow-pass vlan 100 # interface Ethernet0/0/3 port link-type trunk port trunk allow-pass vlan 100 # -
SwitchC | SwitchD
# interface Ethernet0/0/1 port link-type trunk port trunk allow-pass vlan 100 # interface Ethernet0/0/2 port link-type trunk port trunk allow-pass vlan 100 #
-
-
验证Ubuntu与各交换机的连通性
$ ssh-keygen -f "/home/sht/.ssh/known_hosts" -R "192.168.50.100" # $ ssh-keygen -f "/home/sht/.ssh/known_hosts" -R "192.168.50.101" # $ ssh-keygen -f "/home/sht/.ssh/known_hosts" -R "192.168.50.102" # $ ssh-keygen -f "/home/sht/.ssh/known_hosts" -R "192.168.50.103"如果都能成功登录,则说明Ubuntu与交换机连通性良好,并且可以进行远程登录;
编写脚本并上传到设备
脚本总览
-
配置内容总览
-
脚本总览
#!/usr/bin/env python3 import paramiko import time SwitchA = { 'ip': '192.168.50.100', 'username': 'sht', 'password': 'Huawei', 'is_root': True, 'primary_instances': ['1'], 'secondary_instances': ['2'], 'cost_change_interfaces': [], 'trunk_interfaces': ['e0/0/1', 'e0/0/2'], 'access_interfaces': [], 'root_interfaces':['e0/0/2'], } SwitchB = { 'ip': '192.168.50.101', 'username': 'sht', 'password': 'Huawei', 'is_root': True, 'primary_instances': ['2'], 'secondary_instances': ['1'], 'cost_change_interfaces': [], 'trunk_interfaces': ['e0/0/1', 'e0/0/2'], 'access_interfaces': [], 'root_interfaces':['e0/0/2'], } SwitchC = { 'ip': '192.168.50.102', 'username': 'sht', 'password': 'Huawei', 'is_root': False, 'primary_instances': ['1'], 'secondary_instances': ['2'], 'cost_change_interfaces': ['e0/0/2'], 'trunk_interfaces': ['e0/0/1', 'e0/0/2'], 'access_interfaces': ['e0/0/3'], 'root_interfaces':[], } SwitchD = { 'ip': '192.168.50.103', 'username': 'sht', 'password': 'Huawei', 'is_root': False, 'primary_instances': ['2'], 'secondary_instances': ['1'], 'cost_change_interfaces': ['e0/0/2'], 'trunk_interfaces': ['e0/0/1', 'e0/0/2'], 'access_interfaces': ['e0/0/3'], 'root_interfaces':[], } def paramiko_SSH_Command(switch): ''' Start ''' ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # not recommended for production but fine for labs ssh_client.connect(hostname = switch['ip'], username = switch['username'], password = switch['password']) print(" "*3) print("==============="*3) print(" "*3) print("Successful connection", switch['ip']) remote_connection = ssh_client.invoke_shell() remote_connection.send("system\n") ''' Some Configuration Functions ''' common_Configuration(remote_connection, switch['cost_change_interfaces'], switch['secondary_instances']) if switch['is_root']: root_PriSec(remote_connection, switch['primary_instances'], switch['secondary_instances']) edged_port_bpdu_protection(remote_connection, switch['access_interfaces']) if switch['root_interfaces']: root_protection(remote_connection, switch['root_interfaces']) conf_trunk_interfaces(remote_connection, switch['trunk_interfaces']) if switch == SwitchC: conf_access_interfaces(remote_connection, switch['access_interfaces'], 2) elif switch == SwitchD: conf_access_interfaces(remote_connection, switch['access_interfaces'], 11) ''' End ''' remote_connection.send("return\n") remote_connection.send("save\n") remote_connection.send("y\n") remote_connection.send("\n") time.sleep(5) output = remote_connection.recv(65535) print(output.decode('ascii')) ssh_client.close def common_Configuration(remote_connection, cost_change_interfaces, secondary_instances): # Stp region_configuration print("Configuring stp region_configuration ...") region_configurations = ['stp region-configuration\n', 'region-name RG1\n', 'instance 1 vlan 2 to 10\n', 'instance 2 vlan 11 to 20\n', 'active region-configuration\n', 'quit\n'] for region_configuration in region_configurations: remote_connection.send(region_configuration) # Stp cost computing methods print("Configuring stp pathcost-standard legacy ...") remote_connection.send("stp pathcost-standard legacy\n") # Stp interface cost print("Configuring interface cost ...") if cost_change_interfaces: for secondary_instance in secondary_instances: for cost_change_interface in cost_change_interfaces: remote_connection.send("interface " + cost_change_interface + "\n") remote_connection.send("stp instance " + secondary_instance +" cost 20000\n") remote_connection.send("quit\n") # STP enable print("Creating VLAN and Enabling STP ...") remote_connection.send("vlan batch 2 to 20\n") remote_connection.send("stp enable\n") def conf_trunk_interfaces(remote_connection, trunk_interfaces): print("Configuring trunk interfaces ...") for trunk_interface in trunk_interfaces: remote_connection.send("interface " + trunk_interface + "\n") remote_connection.send("port link-type trunk\n") remote_connection.send("port trunk allow-pass vlan 2 to 20\n") remote_connection.send("quit\n") def conf_access_interfaces(remote_connection, access_interfaces, access_vlan): print("Configuring access interfaces ...") for access_interface in access_interfaces: remote_connection.send("interface " + access_interface + "\n") remote_connection.send("port link-type access\n") remote_connection.send("port default vlan " + str(access_vlan) + "\n") remote_connection.send("quit\n") def root_protection(remote_connection, interfaces): print("Configuring stp protection ...") for interface in interfaces: remote_connection.send("interface " + interface + "\n") remote_connection.send("stp root-protection\n") remote_connection.send("quit\n") def edged_port_bpdu_protection(remote_connection, interfaces): print("Configuring edged-port and bpdu protection ...") if interfaces: for interface in interfaces: remote_connection.send("interface " + interface + "\n") remote_connection.send("stp edged-port enable\n") remote_connection.send("quit\n") remote_connection.send("stp bpdu-protection\n") def root_PriSec(remote_connection, primary_instances, secondary_instances): # Stp primary root print("Configuring primary root ...") if primary_instances: for primary_instance in primary_instances: remote_connection.send("stp instance " + primary_instance + " root primary\n") else: print("No primary root configuration.") # Stp secondary root print("Configuring secondary root ...") if secondary_instances: for secondary_instance in secondary_instances: remote_connection.send("stp instance " + secondary_instance + " root secondary\n") else: print("No secondary root configuration.") if __name__=="__main__": paramiko_SSH_Command(SwitchA) paramiko_SSH_Command(SwitchB) paramiko_SSH_Command(SwitchC) paramiko_SSH_Command(SwitchD)
将配置推送到设备
脚本解析
- 使用字典(dict)来规范交换机的配置参数
- 接入层与汇聚层在某些配置上不同,而字典中的键是相同的
- 为了后续调用不同方法配置不同配置,没有相应配置的key都设置了空值,函数在被调用时先判断是否为空值;如果为空,则不进行特定功能的配置;如果不为空,则根据键(key)来调用值(value),进行相应功能的配置;
- 使用
paramiko_SSH_Command(switch)作为一个总方法来调用其余对应各类配置的方法- 四台交换机的stp region-configuraion配置相同,并且都需要创建相同的VLAN然后使能STP(默认MSTP模式);除此以外,STP开销都使用华为特有的计算方法;用一个函数
common_Configuration封装; - 配置access接口时,汇聚层的SwitchA和SwitchB不需要配置,所以调用
conf_access_interfaces(remote_connection, access_interfaces, access_vlan)时,先判断switch对象是否为SwitchC或SwitchD;根据不同Switch指定的接口PVID也不同,比如SwitchC负责偶数vlan下的主机,而SwitchD负责奇数vlan下的主机,通过匹配SwitchC还是SwitchD,来配置不同的PVID; - 类似于边缘端口的配置
edged_port_bpdu_protection,不受一些特殊条件限制 (如不同Switch配置不同PVID),只是每个交换机上需要配置的接口不同,使能边缘端口命令没有其他参数的参与,就可以先判断需要配置边缘端口的列表是否为空,为空则不进行配置,不为空则for循环调用待配置接口列表,各个接口挨个配置;同理,MSTP根和备用根的配置,也只在SwitchA和SwitchB上进行,通过判断是否为空,就可以避免在SwitchC和SwitchD上配置,然后进一步根据设备字典中的instance相关字段,只在汇聚层设备上调用root_PriSec。
- 四台交换机的stp region-configuraion配置相同,并且都需要创建相同的VLAN然后使能STP(默认MSTP模式);除此以外,STP开销都使用华为特有的计算方法;用一个函数
验证配置
- 查看端口状态和端口保护类型
-
SwitchA
[SwitchA]display stp brief MSTID Port Role STP State Protection ... 1 Ethernet0/0/1 DESI FORWARDING NONE 1 Ethernet0/0/2 DESI FORWARDING ROOT 2 Ethernet0/0/1 ROOT FORWARDING NONE 2 Ethernet0/0/2 DESI FORWARDING ROOT- 在MSTI1中,SwitchA为根桥,则Ethernet0/0/1和Ethernet0/0/2成为指定端口;
- 在MSTI2中,SwitchA的Ethernet0/0/1成为指定端口,Ethernet0/0/2成为根端口;
-
SwitchB
[SwitchB]display stp brief MSTID Port Role STP State Protection ... 1 Ethernet0/0/1 ROOT FORWARDING NONE 1 Ethernet0/0/2 DESI FORWARDING ROOT 2 Ethernet0/0/1 DESI FORWARDING NONE 2 Ethernet0/0/2 DESI FORWARDING ROOT- 在MSTI2中,SwitchB为根桥,则Ethernet0/0/1和Ethernet0/0/2成为指定端口;
- 在MSTI1中,SwitchB的Ethernet0/0/2成为指定端口,Ethernet0/0/1成为根端口;
-
SwitchC
<SwitchC>display stp interface Ethernet 0/0/1 brief MSTID Port Role STP State Protection .. 1 Ethernet0/0/1 ROOT FORWARDING NONE 2 Ethernet0/0/1 ROOT FORWARDING NONE <SwitchC>display stp interface Ethernet 0/0/2 brief MSTID Port Role STP State Protection ... 1 Ethernet0/0/2 DESI FORWARDING NONE 2 Ethernet0/0/2 ALTE DISCARDING NONE- 在MSTI1和MSTI2中,Ethernet0/0/1都是根端口;
- 在MST1中,Ethernet0/0/2为指定端口;在MST2中,Ethernet0/0/1被阻塞;
-
SwitchD
<SwitchD>display stp interface e0/0/1 brief MSTID Port Role STP State Protection ... 1 Ethernet0/0/1 ROOT FORWARDING NONE 2 Ethernet0/0/1 ROOT FORWARDING NONE <SwitchD>display stp interface e0/0/2 brief MSTID Port Role STP State Protection ... 1 Ethernet0/0/2 ALTE DISCARDING NONE 2 Ethernet0/0/2 DESI FORWARDING NONE- 在MSTI1和MSTI2中,Ethernet0/0/1都是根端口;
- 在MST2中,Ethernet0/0/2为指定端口;在MST1中,Ethernet0/0/1被阻塞;
-
参考
- Paramiko documentation
- 华为S3700配置文档
写在最后
- 本文主要在记录自己学习paramiko的过程,旨在提高自己对paramiko的认识和编程能力,规范性不一定强,只是一种探索方式,可能并不能作为一种可靠的学习参考;
- 文中有很多内容是转译和自己的所思所想,可能存在多处错误,也拜托大家能够指出错误和不足之处。
往期内容
网络基础回顾
【1】交换机数据处理流程