网络自动化[2] - 在eNSP上使用Paramiko配置MSTP场景

953 阅读10分钟

测试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
      
    • 解决方案:

      来源:

      OpenSSH: Legacy Options

      $ 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.close
    

    paramiko_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会话的高级表示,该类封装了TransportChannelSFTPClient类来处理好认证和打开通道(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()hostnamessh.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_keysload_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)的通道
      • remote_connection.send()
        • 通道(Channel)类中的send()用于传输数据
        • 这里remote_connection是通道(channel),调用send()方法向SSH服务器发送数据
    • 第三部分

      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_clientclose直接关闭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

验证配置

  • 查看端口状态和端口保护类型
    • 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的过程,旨在提高自己对paramiko的认识和编程能力,规范性不一定强,只是一种探索方式,可能并不能作为一种可靠的学习参考;
  • 文中有很多内容是转译和自己的所思所想,可能存在多处错误,也拜托大家能够指出错误和不足之处。

往期内容

【1】使用华为模拟器eNSP搭建网络自动化场景

网络基础回顾

【1】交换机数据处理流程