本文主要是讲解netmiko的两方面扩展,一个是支持platform的扩展,另一个是连接驱动的扩展。以下就是锐捷的Red-Giant系列作为案例进行一一阐述。
1.平台扩展
对于锐捷的Red-Giant系列,netmiko默认是支持的,不支持方面包括:
-
telnet的登录无法兼容,由于采用老版本协议。
-
交互命令和ruijie_os(默认)存在区别
实现方式:针对CLASS_MAPPER_BASE,CLASS_MAPPER的扩展
1.1.编码设计锐捷的扩展包
1.1.1. 项目说明
项目名称:netmiko_extend,
项目结构:
1.1.1. 源码说明
ruijie_rg.py
"""Ruijie RGOS Support"""
import sys
import time
from typing import TYPE_CHECKING
from netmiko.channel import Channel
from netmiko.no_enable import NoEnable
from netmiko.utilities import m_exec_time # noqa
if TYPE_CHECKING:
pass
from typing import Any
from netmiko.cisco_base_connection import CiscoBaseConnection
class RuijieRGBase(NoEnable,CiscoBaseConnection):
def session_preparation(self) -> None:
"""Prepare the session after the connection has been established."""
self._test_channel_read(pattern=r"[>#]")
self.set_base_prompt()
"""Ruijie OS requires enable mode to set terminal width"""
self.enable()
self.set_terminal_width(command="terminal width 256", pattern="terminal")
self.disable_paging(command="terminal length 0")
# Clear the read buffer
time.sleep(0.3 * self.global_delay_factor)
self.clear_buffer()
def save_config(self, cmd: str = "write", confirm: bool = False, confirm_response: str = "") -> str:
"""Save config: write"""
return super().save_config(cmd=cmd, confirm=confirm, confirm_response=confirm_response)
def establish_connection(self, width: int = 511, height: int = 1000) -> None:
self.channel: Channel
if self.protocol == "telnet":
import pexpect
import pexpect.popen_spawn
from netmiko_extend.expect_channel import ExpectChannel
# linux系统采用以下判断
if sys.platform.startswith('linux'):
self.remote_conn = pexpect.spawn('telnet {} {}'.format(self.host, self.port), timeout=self.timeout)
# win系统采用以下判断
elif sys.platform.startswith('win'):
self.remote_conn = pexpect.popen_spawn.PopenSpawn(
"tools/plink.exe -telnet {} -P {}".format(self.host, self.port), timeout=self.timeout)
# 忽略大小,在匹配的时候
self.remote_conn.ignorecase = True
# self.remote_conn.logfile_read = sys.stdout
# Migrating communication to channel class
self.channel = ExpectChannel(conn=self.remote_conn, encoding=self.encoding)
self.telnet_login()
return None
else:
return super().establish_connection(width=width, height=height)
class RuijieRGSSH(RuijieRGBase):
pass
class RuijieRGTelnet(RuijieRGBase):
def __init__(self, *args: Any, **kwargs: Any) -> None:
default_enter = kwargs.get("default_enter")
kwargs["default_enter"] = "\r\n" if default_enter is None else default_enter
super().__init__(*args, **kwargs)
expect_channel.py
采用pexcept扩展支持telnet登录吗,属于驱动扩展,在第二部分详细讲解,此时先跳过
platform_register.py
平台注册类型两两个字典数据的扩展
from netmiko_extend.ruijie.ruijie_rg import RuijieRGSSH, RuijieRGTelnet
class_mapper_base_extend = {
"ruijie_rg": RuijieRGSSH
}
class_mapper_extend = {
"ruijie_rg_telnet": RuijieRGTelnet
}
1.2.修改netmiko源码
1.2.1.增加适配处理文件到源码库
增加适配处理文件: register_extend_adapt.py
文件路径:/{项目工程路径}/venv/lib/python3.10/site-packages/netmiko/register_extend_adapt.py
from netmiko_extend.platform_register import class_mapper_base_extend, class_mapper_extend
def extend_class_mapper_base(CLASS_MAPPER_BASE):
for k, v in class_mapper_base_extend.items():
CLASS_MAPPER_BASE[k] = v
def extend_class_mapper(CLASS_MAPPER):
for k, v in class_mapper_extend.items():
CLASS_MAPPER[k] = v
1.2.1.修改平台映射驱动的源码
修改源码文件: 增加适配处理文件: ssh_dispatcher.py
文件路径:{项目工程路径}/venv/lib/python3.10/site-packages/netmiko/ssh_dispatcher.py
# 增加引用
from netmiko.register_extend_adapt import extend_class_mapper_base,extend_class_mapper
。。。。。。
# 扩展CLASS_MAPPER_BASE,在CLASS_MAPPER_BASE初始设置后面(也就是“FILE_TRANSFER_MAP = {”该行之前)追加
extend_class_mapper_base(CLASS_MAPPER_BASE);
。。。。。。
# 扩展CLASS_MAPPER,在CLASS_MAPPER初始设置后面(也就是“platforms = list(CLASS_MAPPER.keys())”改行之前)追加以下一行代码,
extend_class_mapper(CLASS_MAPPER)
2.链接驱动扩展
由于锐捷Red-Giant系列在netmiko中的telent登录无法实现,所以在此扩展实现telnet登录实现。
经过技术选择采用pexpect交互包实现,但是该包在window的直接使用有一定的问题:
- 采用pexpect调用telnet客户端实现等,直接子程序退出了
所以抛弃了telnet客户端,采用plink.exe作为expect的子进程处理,好处有很多
-
不需要再window安装telnet客户端
-
通过plink这个模块进行不同系统的适配
2.1.拷贝plink.exe 到项目工程中
路径:{项目工程路径}/tools/plink.exe
或者将plink.exe拷贝到在python的安装目录下
关于plink
- plink是什么
PuTTY是一个Telnet、SSH、rlogin、纯TCP以及串行接口连接软件。
plink是可以独立使用的exe实现形式,可以让我们直接在命令行制定好命令,然后执行,完成后自动关闭session。
plink是多种通道协议(ssh,telnet)的一个实现。
- 下载地址
官方主页 http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html
下载地址 https://the.earth.li/~sgtatham/putty/latest/w64/plink.exe
2.1.实现一个基于expect的连接驱动
文件问:expect_channel.py,路径在第一部分中已经说过。
import sys
import time
from typing import Optional
import pexpect
import pexpect.popen_spawn
if sys.platform.startswith('linux'):
from pexpect import spawn as spawnmix
elif sys.platform.startswith('win'):
from pexpect.popen_spawn import PopenSpawn as spawnmix
from netmiko.channel import Channel
from netmiko.exceptions import WriteException
class ExpectChannel(Channel):
# def __init__(self, conn: Optional[spawn], encoding: str) -> None:
def __init__(self, conn: Optional[spawnmix], encoding: str) -> None:
"""
Placeholder __init__ method so that reading and writing can be moved to the
channel class.
"""
self.remote_conn = conn
# FIX: move encoding to GlobalState object?
self.encoding = encoding
def write_channel(self, out_data: str) -> None:
if self.remote_conn is None:
raise WriteException(
"Attempt to write data, but there is no active channel."
)
out_data = out_data.rstrip('\r\n')
if sys.platform.startswith('linux'):
# 在linux系统直接使用了pexpect,回车处理交给了pexpect
self.remote_conn.sendline(out_data)
elif sys.platform.startswith('win'):
if "plink.exe" in str(self.remote_conn.proc):
# 在windows系统虽然使用了pexpect,其他协议实现通道使用了plink,回车处理需要兼容plink
self.remote_conn.send(out_data + "\r")
else:
self.remote_conn.sendline(out_data)
def read_buffer(self) -> str:
"""Single read of available data."""
raise NotImplementedError
def read_channel(self, timeout=10) -> str:
output_lines = ''
spawn = self.remote_conn
if timeout is not None:
end_time = time.time() + timeout
trytimes = 0
try:
while True:
# No match at this point
if (timeout is not None) and (timeout < 0):
return output_lines
# Still have time left, so read more data
if sys.platform.startswith('linux'):
# 在linux系统直接使用了pexpect,回车处理交给了pexpect
incoming = spawn.read_nonblocking(spawn.maxread, 0)
elif sys.platform.startswith('win'):
# 在windows系统虽然使用了pexpect,其他协议实现通道使用了plink,回车处理需要兼容plink
incoming = spawn.read_nonblocking(spawn.maxread, 0.1)
if not incoming:
trytimes += 1
if trytimes == 3:
return output_lines
else:
raise
output_lines += incoming.decode(self.encoding, "ignore")
if spawn.delayafterread is not None:
time.sleep(spawn.delayafterread)
if timeout is not None:
timeout = end_time - time.time()
except pexpect.EOF as e:
return output_lines
except pexpect.TIMEOUT as e:
return output_lines
except Exception as error:
raise
2.2.platform建立连接重新实现
核心就是实现将指定platform的remot_conn 采用 ExpectChannel实现
代码详细见的ruijie_rg.py的establish_connection的方法实现
。。。。。。
def establish_connection(self, width: int = 511, height: int = 1000) -> None:
self.channel: Channel
if self.protocol == "telnet":
import pexpect
import pexpect.popen_spawn
from netmiko_extend.expect_channel import ExpectChannel
# linux系统采用以下判断
if sys.platform.startswith('linux'):
self.remote_conn = pexpect.spawn('telnet {} {}'.format(self.host, self.port), timeout=self.timeout)
# win系统采用以下判断
elif sys.platform.startswith('win'):
self.remote_conn = pexpect.popen_spawn.PopenSpawn(
"tools/plink.exe -telnet {} -P {}".format(self.host, self.port), timeout=self.timeout)
# 忽略大小,在匹配的时候
self.remote_conn.ignorecase = True
# self.remote_conn.logfile_read = sys.stdout
# Migrating communication to channel class
self.channel = ExpectChannel(conn=self.remote_conn, encoding=self.encoding)
self.telnet_login()
return None
else:
return super().establish_connection(width=width, height=height)
。。。