Xshell插件开发挑战:用Python打造你的专属网络管理利器
·
文章目录

每日一句正能量
生活在这个世界上,没有人一帆风顺,也没有人天天幸运。有惊喜,有意外;有成功,有失败。只有吃过命运的苦,才会享受到生活的甜。
前言
摘要: Xshell作为Windows平台最主流的SSH客户端,其内置的脚本和插件机制常被忽视。本文将深入Xshell的Python扩展接口,从基础的会话自动化到复杂的网络设备批量管理平台,带你解锁终端工具的二次开发能力,让重复的运维工作自动化、智能化。
目录
- 一、Xshell架构解析:为什么需要插件开发
- 二、环境准备:Xshell Python引擎深度配置
- 三、基础实战:会话自动化与批量操作
- 四、进阶开发:网络设备信息采集平台
- 五、高级应用:可视化网络拓扑管理
- 六、安全增强:密钥管理与审计日志
- 七、性能优化:异步并发与资源控制
- 八、打包部署:插件分发与版本管理
- 九、实战案例:数据中心网络巡检系统
一、Xshell架构解析:为什么需要插件开发
1.1 Xshell的技术架构
Xshell采用分层架构设计,其脚本引擎基于Python 2.7/3.x嵌入式解释器,通过COM接口与主程序交互:
┌─────────────────────────────────────────┐
│ Xshell主程序 (Xshell.exe) │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ 会话管理器 │ │ UI渲染引擎 │ │
│ │ (Session) │ │ (MFC/Qt) │ │
│ └──────┬──────┘ └─────────────────┘ │
│ │ │
│ ┌──────┴───────────────────────────┐ │
│ │ 脚本引擎层 (Scripting) │ │
│ │ ┌─────────┐ ┌───────────────┐ │ │
│ │ │ Python │ │ VBScript/JScript │ │ │
│ │ │ Engine │ │ Engine │ │ │
│ │ └────┬────┘ └───────────────┘ │ │
│ └───────┼──────────────────────────┘ │
│ │ │
│ ┌───────┴───────────────────────────┐ │
│ │ 扩展接口层 (XAPI) │ │
│ │ - Session对象 (连接/断开/发送) │ │
│ │ - Screen对象 (屏幕捕获/解析) │ │
│ │ - Dialog对象 (UI交互) │ │
│ │ - FileTransfer对象 (SFTP/SCP) │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
1.2 插件开发的典型场景
| 场景 | 原生Xshell限制 | 插件解决方案 |
|---|---|---|
| 批量设备巡检 | 只能逐个会话操作 | Python脚本批量并发执行 |
| 配置合规检查 | 无自动化比对能力 | 自动采集+基线比对+报告生成 |
| 密码轮换 | 手动修改数百台设备 | 自动化SSH密钥分发与验证 |
| 网络拓扑发现 | 无网络可视化能力 | 采集LLDP/CDP信息生成拓扑图 |
| 审计合规 | 本地日志易丢失 | 实时上传操作日志到SIEM |
二、环境准备:Xshell Python引擎深度配置
2.1 Python环境配置
Xshell 7+支持Python 3.8+,但需要手动配置:
# 检查Xshell内置Python版本
# 在Xshell中:工具 -> 脚本 -> 运行 -> 输入以下代码
import sys
import platform
print(f"Python版本: {platform.python_version()}")
print(f"解释器路径: {sys.executable}")
print(f"系统路径: {sys.path}")
# 输出示例:
# Python版本: 3.8.10
# 解释器路径: C:\Program Files (x86)\NetSarang\Xshell 7\Python\python.exe
# 系统路径: ['', 'C:\\Program Files (x86)\\NetSarang\\Xshell 7\\Python\\python38.zip', ...]
2.2 扩展包安装
Xshell使用独立的Python环境,需要手动安装第三方库:
:: 以管理员身份运行CMD,进入Xshell Python目录
cd "C:\Program Files (x86)\NetSarang\Xshell 7\Python"
:: 安装必要的扩展包
.\python.exe -m pip install paramiko netmiko textfsm jinja2 openpyxl
.\python.exe -m pip install pyvis networkx matplotlib :: 用于拓扑可视化
:: 验证安装
.\python.exe -c "import paramiko; print(paramiko.__version__)"
2.3 开发目录结构
XshellPlugins/
├── core/ # 核心框架
│ ├── __init__.py
│ ├── session_manager.py # 会话管理封装
│ ├── device_connector.py # 设备连接基类
│ └── logger.py # 统一日志
├── plugins/ # 功能插件
│ ├── __init__.py
│ ├── network_discovery.py # 网络发现
│ ├── config_backup.py # 配置备份
│ ├── compliance_check.py # 合规检查
│ └── password_rotation.py # 密码轮换
├── templates/ # 配置模板
│ ├── cisco_base_config.txt
│ ├── huawei_base_config.txt
│ └── report_template.html
├── output/ # 输出目录
│ ├── logs/
│ ├── backups/
│ └── reports/
└── xshell_plugin_main.py # Xshell入口脚本
三、基础实战:会话自动化与批量操作
3.1 Xshell Python API核心对象
# xshell_api_wrapper.py - Xshell API封装层
# 在Xshell中运行,使用xsh.Session等内置对象
import time
import re
class XshellSession:
"""
封装Xshell会话操作,提供Pythonic接口
"""
def __init__(self, session_name=None):
self.session = xsh.Session if session_name is None else xsh.Sessions(session_name)
self.screen = xsh.Screen
self.dialog = xsh.Dialog
def connect(self, hostname, username, password, port=22):
"""建立新连接"""
try:
self.session.Open(f"ssh://{username}:{password}@{hostname}:{port}")
time.sleep(2) # 等待连接建立
return self.is_connected()
except Exception as e:
print(f"连接失败: {e}")
return False
def is_connected(self):
"""检查连接状态"""
return self.session.Connected
def send_command(self, command, wait_for_prompt=True, timeout=30):
"""
发送命令并等待响应
"""
if not self.is_connected():
raise ConnectionError("会话未连接")
# 发送命令
self.screen.Send(command + "\r")
if not wait_for_prompt:
return None
# 等待提示符出现(支持多厂商)
prompts = [
r'[>#]\s*$', # Cisco/Huawei/Generic
r'\$\s*$', # Linux普通用户
r'%\s*$', # Juniper
r']\s*$', # H3C
]
start_time = time.time()
while time.time() - start_time < timeout:
# 获取当前屏幕内容
screen_content = self.screen.Get(1, 1, self.screen.Rows, self.screen.Columns)
for prompt in prompts:
if re.search(prompt, screen_content, re.MULTILINE):
# 提取命令输出(去掉命令本身和提示符)
lines = screen_content.strip().split('\n')
# 过滤掉命令行和最后的提示符
output_lines = []
capture = False
for line in lines:
if command in line:
capture = True
continue
if re.search(prompt, line):
break
if capture:
output_lines.append(line)
return '\n'.join(output_lines)
time.sleep(0.5)
raise TimeoutError(f"等待提示符超时({timeout}秒)")
def send_config_commands(self, commands, save_config=True):
"""
发送配置命令序列(自动进入配置模式)
"""
# 检测设备类型并进入配置模式
hostname = self.send_command("show version") # Cisco
if "% Invalid" in hostname:
hostname = self.send_command("display version") # Huawei/H3C
# 进入配置模式
config_entry = "configure terminal" if "Cisco" in hostname else "system-view"
self.send_command(config_entry)
# 逐条发送配置
for cmd in commands:
output = self.send_command(cmd)
if "error" in output.lower() or "invalid" in output.lower():
print(f"配置命令失败: {cmd}")
print(f"错误信息: {output}")
# 保存配置
if save_config:
save_cmd = "write memory" if "Cisco" in hostname else "save"
self.send_command(save_cmd)
self.screen.Send("y\r") # 确认保存
def disconnect(self):
"""断开连接"""
if self.is_connected():
self.session.Close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
return False
3.2 批量设备操作框架
# batch_executor.py - 批量执行框架
import threading
import queue
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from typing import List, Callable, Optional
@dataclass
class Device:
hostname: str
ip: str
vendor: str # cisco/huawei/h3c/juniper/linux
username: str
password: str
port: int = 22
enable_password: Optional[str] = None
@dataclass
class TaskResult:
device: Device
success: bool
output: str
error_message: Optional[str] = None
execution_time: float = 0.0
class BatchExecutor:
"""
批量设备操作执行器
支持并发控制和结果收集
"""
def __init__(self, max_workers=10, timeout=300):
self.max_workers = max_workers
self.timeout = timeout
self.results = []
self.lock = threading.Lock()
def execute_on_devices(self, devices: List[Device],
task_func: Callable[[Device], TaskResult]) -> List[TaskResult]:
"""
在多个设备上并行执行任务
Args:
devices: 设备列表
task_func: 任务函数,接收Device对象返回TaskResult
"""
self.results = []
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# 提交所有任务
future_to_device = {
executor.submit(self._wrapped_task, device, task_func): device
for device in devices
}
# 收集结果
for future in as_completed(future_to_device, timeout=self.timeout):
device = future_to_device[future]
try:
result = future.result()
with self.lock:
self.results.append(result)
except Exception as e:
error_result = TaskResult(
device=device,
success=False,
output="",
error_message=str(e),
execution_time=0.0
)
with self.lock:
self.results.append(error_result)
return self.results
def _wrapped_task(self, device: Device, task_func) -> TaskResult:
"""包装任务以捕获异常"""
import time
start = time.time()
try:
result = task_func(device)
result.execution_time = time.time() - start
return result
except Exception as e:
return TaskResult(
device=device,
success=False,
output="",
error_message=str(e),
execution_time=time.time() - start
)
def generate_report(self, output_file: str):
"""生成JSON格式的执行报告"""
report = {
"summary": {
"total": len(self.results),
"success": sum(1 for r in self.results if r.success),
"failed": sum(1 for r in self.results if not r.success),
"total_time": sum(r.execution_time for r in self.results)
},
"details": [
{
"hostname": r.device.hostname,
"ip": r.device.ip,
"success": r.success,
"output": r.output[:1000] if r.success else r.error_message, # 截断
"execution_time": r.execution_time
}
for r in self.results
]
}
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
return report
# 使用示例:批量收集设备版本信息
def collect_version_task(device: Device) -> TaskResult:
"""收集设备版本信息的任务函数"""
try:
with XshellSession() as session:
connected = session.connect(device.ip, device.username, device.password, device.port)
if not connected:
return TaskResult(device, False, "", "连接失败")
# 根据厂商选择命令
version_cmd = {
'cisco': 'show version',
'huawei': 'display version',
'h3c': 'display version',
'juniper': 'show version',
'linux': 'uname -a && cat /etc/os-release'
}.get(device.vendor, 'show version')
output = session.send_command(version_cmd, timeout=60)
return TaskResult(device, True, output)
except Exception as e:
return TaskResult(device, False, "", str(e))
# 在Xshell中执行批量任务
def main():
# 加载设备列表
devices = [
Device("SW-Core-01", "192.168.1.1", "cisco", "admin", "password123"),
Device("SW-Core-02", "192.168.1.2", "cisco", "admin", "password123"),
Device("FW-Border-01", "192.168.1.10", "huawei", "admin", "password123"),
# ... 更多设备
]
executor = BatchExecutor(max_workers=5, timeout=120)
results = executor.execute_on_devices(devices, collect_version_task)
# 生成报告
report = executor.generate_report("version_collection_report.json")
# 输出摘要
print(f"\n执行完成:")
print(f" 成功: {report['summary']['success']}/{report['summary']['total']}")
print(f" 失败: {report['summary']['failed']}")
print(f" 总耗时: {report['summary']['total_time']:.2f}秒")
print(f"\n详细报告已保存至: version_collection_report.json")
if __name__ == "__main__":
main()
四、进阶开发:网络设备信息采集平台
4.1 基于TextFSM的解析引擎
网络设备的命令输出是半结构化文本,使用TextFSM模板解析:
# textfsm_parser.py - 网络设备输出解析
import textfsm
from io import StringIO
# Cisco show version 模板
CISCO_VERSION_TEMPLATE = """
Value Model (\S+)
Value Version (\S+)
Value Image (\S+)
Value Uptime (.+)
Value Serial (\S+)
Value Config_Register (\S+)
Start
^Cisco IOS Software.*Version\s+${Version},
^.*Model number\s*:\s*${Model}
^.*System serial number\s*:\s*${Serial}
^.*uptime is\s+${Uptime}
^Configuration register is\s+${Config_Register}
"""
# Huawei display version 模板
HUAWEI_VERSION_TEMPLATE = """
Value Model (\S+)
Value Version (\S+)
Value Uptime (.+)
Start
^Huawei Versatile Routing Platform Software
^VRP \(R\) software, Version\s+${Version}
^HUAWEI\s+${Model}\s+uptime is\s+${Uptime}
"""
class NetworkDataParser:
"""
网络设备数据解析器
"""
def __init__(self):
self.templates = {
'cisco': {
'version': CISCO_VERSION_TEMPLATE,
'interfaces': self._load_template('cisco_show_interfaces.textfsm'),
'arp': self._load_template('cisco_show_arp.textfsm'),
'mac': self._load_template('cisco_show_mac.textfsm'),
},
'huawei': {
'version': HUAWEI_VERSION_TEMPLATE,
'interfaces': self._load_template('huawei_display_interface.textfsm'),
}
}
def _load_template(self, filename):
"""从文件加载TextFSM模板"""
try:
with open(f"templates/{filename}", 'r') as f:
return f.read()
except FileNotFoundError:
return None
def parse(self, vendor: str, command: str, raw_output: str) -> list:
"""
解析命令输出
Args:
vendor: 设备厂商
command: 命令类型
raw_output: 原始命令输出
"""
template_str = self.templates.get(vendor, {}).get(command)
if not template_str:
return [{"raw": raw_output}] # 无模板时返回原始数据
try:
template = textfsm.TextFSM(StringIO(template_str))
parsed = template.ParseText(raw_output)
# 转换为字典列表
result = []
headers = template.header
for row in parsed:
result.append(dict(zip(headers, row)))
return result
except Exception as e:
return [{"error": str(e), "raw": raw_output[:500]}]
def parse_interfaces(self, vendor: str, raw_output: str) -> list:
"""专门解析接口信息,提取关键指标"""
parsed = self.parse(vendor, 'interfaces', raw_output)
# 标准化接口数据
standardized = []
for item in parsed:
std_item = {
'interface': item.get('INTERFACE') or item.get('Port'),
'status': item.get('STATUS') or item.get('Link'),
'protocol': item.get('PROTOCOL') or item.get('Protocol'),
'description': item.get('DESCRIP') or item.get('Description'),
'input_rate': self._extract_rate(item.get('INPUT_RATE', '0')),
'output_rate': self._extract_rate(item.get('OUTPUT_RATE', '0')),
'errors': int(item.get('INPUT_ERRORS', 0)) + int(item.get('OUTPUT_ERRORS', 0))
}
standardized.append(std_item)
return standardized
def _extract_rate(self, rate_str: str) -> int:
"""从速率字符串提取数值(bps)"""
if not rate_str:
return 0
rate_str = str(rate_str).upper().strip()
multipliers = {'K': 1000, 'M': 1000000, 'G': 1000000000}
for suffix, mult in multipliers.items():
if suffix in rate_str:
try:
return int(float(rate_str.replace(suffix, '').strip()) * mult)
except:
return 0
try:
return int(rate_str)
except:
return 0
4.2 网络资产数据库
# network_inventory.py - 网络资产管理系统
import sqlite3
from datetime import datetime
from typing import List, Optional
from dataclasses import dataclass, asdict
@dataclass
class NetworkAsset:
id: Optional[int] = None
hostname: str = ""
ip_address: str = ""
vendor: str = ""
model: str = ""
software_version: str = ""
serial_number: str = ""
site: str = ""
rack: str = ""
role: str = "" # core/distribution/access/wan/firewall
last_seen: Optional[datetime] = None
config_backup_path: Optional[str] = None
compliance_status: str = "unknown" # pass/fail/unknown
def to_dict(self):
return asdict(self)
class InventoryDB:
"""
SQLite-based网络资产数据库
"""
def __init__(self, db_path="network_inventory.db"):
self.db_path = db_path
self._init_db()
def _init_db(self):
"""初始化数据库表结构"""
with sqlite3.connect(self.db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS assets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hostname TEXT UNIQUE NOT NULL,
ip_address TEXT NOT NULL,
vendor TEXT,
model TEXT,
software_version TEXT,
serial_number TEXT,
site TEXT,
rack TEXT,
role TEXT,
last_seen TIMESTAMP,
config_backup_path TEXT,
compliance_status TEXT DEFAULT 'unknown',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_ip ON assets(ip_address)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_site ON assets(site)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_role ON assets(role)
""")
def upsert_asset(self, asset: NetworkAsset) -> int:
"""插入或更新资产"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.execute("""
INSERT INTO assets (
hostname, ip_address, vendor, model, software_version,
serial_number, site, rack, role, last_seen, compliance_status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(hostname) DO UPDATE SET
ip_address=excluded.ip_address,
vendor=excluded.vendor,
model=excluded.model,
software_version=excluded.software_version,
serial_number=excluded.serial_number,
site=excluded.site,
rack=excluded.rack,
role=excluded.role,
last_seen=excluded.last_seen,
compliance_status=excluded.compliance_status,
updated_at=CURRENT_TIMESTAMP
RETURNING id
""", (
asset.hostname, asset.ip_address, asset.vendor, asset.model,
asset.software_version, asset.serial_number, asset.site,
asset.rack, asset.role, datetime.now(), asset.compliance_status
))
return cursor.fetchone()[0]
def get_assets_by_role(self, role: str) -> List[NetworkAsset]:
"""按角色查询资产"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM assets WHERE role = ?", (role,)
).fetchall()
return [NetworkAsset(**dict(row)) for row in rows]
def get_compliance_report(self) -> dict:
"""生成合规状态报告"""
with sqlite3.connect(self.db_path) as conn:
stats = conn.execute("""
SELECT
compliance_status,
COUNT(*) as count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) as percentage
FROM assets
GROUP BY compliance_status
""").fetchall()
return {
'total_assets': sum(s[1] for s in stats),
'status_breakdown': {
s[0]: {'count': s[1], 'percentage': s[2]}
for s in stats
}
}
def find_outdated_devices(self, days: int = 30) -> List[NetworkAsset]:
"""查找长时间未更新的设备"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute("""
SELECT * FROM assets
WHERE last_seen < datetime('now', '-{} days')
OR last_seen IS NULL
""".format(days)).fetchall()
return [NetworkAsset(**dict(row)) for row in rows]
# 与Xshell集成:自动发现并入库
def auto_discovery_and_inventory(subnet: str):
"""
自动发现网段内设备并录入资产库
"""
from pythonping import ping # pip install pythonping
import socket
db = InventoryDB()
discovered = []
# 简化示例:扫描/24网段
base_ip = subnet.rsplit('.', 1)[0]
for i in range(1, 255):
ip = f"{base_ip}.{i}"
# Ping探测
try:
response = ping(ip, count=1, timeout=0.5)
if response.success():
# 尝试SSH连接获取信息
try:
with XshellSession() as session:
# 尝试常见凭证(实际应使用密钥或Vault)
if session.connect(ip, "admin", "admin", 22):
# 获取主机名
hostname = session.send_command("hostname").strip()
# 创建设备对象
device = Device(hostname, ip, "unknown", "admin", "admin")
# 收集详细信息
result = collect_version_task(device)
if result.success:
# 解析厂商和型号
parser = NetworkDataParser()
parsed = parser.parse('cisco', 'version', result.output)
if parsed:
info = parsed[0]
asset = NetworkAsset(
hostname=hostname,
ip_address=ip,
vendor=info.get('Model', 'unknown'),
model=info.get('Model', 'unknown'),
software_version=info.get('Version', 'unknown'),
serial_number=info.get('Serial', 'unknown'),
last_seen=datetime.now()
)
asset_id = db.upsert_asset(asset)
discovered.append(asset)
print(f"发现设备: {hostname} ({ip}) -> ID {asset_id}")
except Exception as e:
print(f"无法获取 {ip} 的详细信息: {e}")
except Exception as e:
continue
print(f"\n发现完成: 共 {len(discovered)} 台设备")
return discovered
五、高级应用:可视化网络拓扑管理
5.1 基于PyVis的交互式拓扑
# network_topology.py - 网络拓扑可视化
from pyvis.network import Network
import json
from typing import Dict, List, Tuple
class TopologyBuilder:
"""
基于LLDP/CDP信息构建网络拓扑
"""
def __init__(self):
self.nodes = {}
self.edges = []
self.net = Network(height="800px", width="100%", bgcolor="#222222", font_color="white")
def add_device(self, device_id: str, label: str, device_type: str,
site: str = "", ip: str = "", **kwargs):
"""添加设备节点"""
# 根据设备类型设置颜色和图标
colors = {
'core': '#ff6b6b', # 红色
'distribution': '#4ecdc4', # 青色
'access': '#45b7d1', # 蓝色
'wan': '#f9ca24', # 黄色
'firewall': '#6c5ce7', # 紫色
'server': '#a29bfe' # 淡紫
}
self.nodes[device_id] = {
'id': device_id,
'label': label,
'title': f"IP: {ip}<br>Site: {site}<br>Type: {device_type}",
'color': colors.get(device_type, '#95a5a6'),
'size': 30 if device_type == 'core' else 20,
'group': site,
**kwargs
}
def add_link(self, source: str, target: str,
local_port: str = "", remote_port: str = "",
bandwidth: str = "1G", **kwargs):
"""添加连接"""
self.edges.append({
'from': source,
'to': target,
'title': f"{local_port} -> {remote_port}<br>Bandwidth: {bandwidth}",
'label': bandwidth,
'width': 3 if '10G' in bandwidth or '40G' in bandwidth else 1,
'smooth': {'type': 'continuous'},
**kwargs
})
def build_from_lldp_data(self, lldp_results: Dict[str, List[Dict]]):
"""
从LLDP采集结果构建拓扑
lldp_results格式: {
'device_hostname': [
{'local_port': 'Gi0/1', 'neighbor': 'SW-02', 'remote_port': 'Gi0/2', 'platform': 'cisco'},
...
]
}
"""
# 首先添加所有设备节点
all_devices = set(lldp_results.keys())
for neighbors in lldp_results.values():
for n in neighbors:
all_devices.add(n['neighbor'])
# 从资产库获取设备信息
db = InventoryDB()
for device in all_devices:
# 查询数据库获取详细信息
asset = db.get_asset_by_hostname(device)
if asset:
self.add_device(
device_id=device,
label=device,
device_type=asset.role,
site=asset.site,
ip=asset.ip_address
)
else:
# 未知设备
self.add_device(device, device, 'unknown')
# 添加连接
for local_device, neighbors in lldp_results.items():
for neighbor in neighbors:
self.add_link(
source=local_device,
target=neighbor['neighbor'],
local_port=neighbor['local_port'],
remote_port=neighbor['remote_port'],
bandwidth=neighbor.get('bandwidth', '1G')
)
def render(self, output_file: str = "network_topology.html"):
"""生成HTML可视化"""
# 添加所有节点和边
for node in self.nodes.values():
self.net.add_node(**node)
for edge in self.edges:
self.net.add_edge(**edge)
# 配置物理模拟
self.net.set_options("""
{
"physics": {
"forceAtlas2Based": {
"gravitationalConstant": -50,
"centralGravity": 0.01,
"springLength": 100,
"springConstant": 0.08
},
"maxVelocity": 50,
"solver": "forceAtlas2Based",
"timestep": 0.35,
"stabilization": {"iterations": 150}
},
"interaction": {
"hover": true,
"tooltipDelay": 200,
"hideEdgesOnDrag": true
}
}
""")
# 生成HTML
self.net.save_graph(output_file)
print(f"拓扑图已保存至: {output_file}")
return output_file
# 与Xshell集成:自动采集LLDP并生成拓扑
def auto_build_topology(seed_devices: List[Device]):
"""
从种子设备开始,自动发现全网拓扑
"""
lldp_data = {}
visited = set()
to_visit = seed_devices.copy()
while to_visit:
device = to_visit.pop(0)
if device.hostname in visited:
continue
print(f"采集 {device.hostname} 的LLDP信息...")
try:
with XshellSession() as session:
if not session.connect(device.ip, device.username, device.password):
continue
# 根据厂商选择LLDP命令
lldp_cmd = {
'cisco': 'show lldp neighbors detail',
'huawei': 'display lldp neighbor',
'h3c': 'display lldp neighbor-information verbose'
}.get(device.vendor, 'show lldp neighbors')
output = session.send_command(lldp_cmd, timeout=60)
# 解析LLDP输出(简化示例)
neighbors = parse_lldp_output(device.vendor, output)
lldp_data[device.hostname] = neighbors
# 将新发现的设备加入待访问队列
for neighbor in neighbors:
if neighbor['neighbor'] not in visited:
# 尝试从资产库或DNS获取IP
neighbor_ip = resolve_hostname(neighbor['neighbor'])
if neighbor_ip:
new_device = Device(
neighbor['neighbor'],
neighbor_ip,
guess_vendor(neighbor['platform']),
device.username, # 假设相同凭证
device.password
)
to_visit.append(new_device)
visited.add(device.hostname)
except Exception as e:
print(f"采集 {device.hostname} 失败: {e}")
# 构建拓扑
builder = TopologyBuilder()
builder.build_from_lldp_data(lldp_data)
html_path = builder.render("auto_discovered_topology.html")
# 自动打开浏览器
import webbrowser
webbrowser.open(f"file:///{html_path}")
return lldp_data
六、安全增强:密钥管理与审计日志
6.1 集成HashiCorp Vault
# vault_integration.py - 安全凭证管理
import hvac
import os
from cryptography.fernet import Fernet
class SecureCredentialManager:
"""
集成HashiCorp Vault管理设备凭证
支持动态密钥和自动轮换
"""
def __init__(self, vault_addr: str = None, token: str = None):
self.vault_addr = vault_addr or os.getenv('VAULT_ADDR', 'http://localhost:8200')
self.token = token or os.getenv('VAULT_TOKEN')
self.client = hvac.Client(url=self.vault_addr, token=self.token)
if not self.client.is_authenticated():
raise ConnectionError("无法连接到Vault或Token无效")
def get_device_credentials(self, device_hostname: str) -> Dict:
"""
从Vault获取设备凭证
路径: secret/network-devices/{hostname}
"""
secret_path = f"network-devices/{device_hostname}"
try:
response = self.client.secrets.kv.v2.read_secret_version(path=secret_path)
data = response['data']['data']
return {
'username': data.get('username'),
'password': data.get('password'),
'enable_password': data.get('enable_password'),
'ssh_key': data.get('ssh_private_key'),
'last_rotated': data.get('metadata', {}).get('created_time')
}
except hvac.exceptions.InvalidPath:
raise KeyError(f"未找到设备 {device_hostname} 的凭证")
def rotate_password(self, device_hostname: str, new_password: str = None):
"""
自动轮换设备密码
1. 生成新密码
2. 登录设备修改密码
3. 更新Vault
4. 验证新密码
"""
import secrets
import string
# 生成强密码
if not new_password:
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
new_password = ''.join(secrets.choice(alphabet) for _ in range(16))
# 获取当前凭证
current_creds = self.get_device_credentials(device_hostname)
# 连接设备修改密码(以Cisco为例)
device = Device(
hostname=device_hostname,
ip=self._resolve_ip(device_hostname),
vendor='cisco',
username=current_creds['username'],
password=current_creds['password']
)
try:
with XshellSession() as session:
session.connect(device.ip, device.username, device.password)
# 进入配置模式修改密码
session.send_command("configure terminal")
session.send_command(f"username {device.username} privilege 15 secret {new_password}")
session.send_command("end")
session.send_command("write memory")
print(f"设备 {device_hostname} 密码已修改")
# 更新Vault
self.client.secrets.kv.v2.create_or_update_secret(
path=f"network-devices/{device_hostname}",
secret={
'username': device.username,
'password': new_password,
'enable_password': current_creds.get('enable_password'),
'last_rotated': datetime.now().isoformat()
}
)
# 验证新密码
test_session = XshellSession()
if test_session.connect(device.ip, device.username, new_password):
print("新密码验证成功")
return True
else:
raise RuntimeError("新密码验证失败,可能需要手动恢复")
except Exception as e:
print(f"密码轮换失败: {e}")
# 发送告警通知管理员
self._send_alert(f"Password rotation failed for {device_hostname}: {e}")
raise
def _resolve_ip(self, hostname: str) -> str:
"""解析主机名到IP"""
import socket
return socket.gethostbyname(hostname)
def _send_alert(self, message: str):
"""发送告警(集成企业微信/钉钉/Slack)"""
# 实际实现略
print(f"[ALERT] {message}")
# Xshell脚本中使用Vault凭证
def secure_connect_example():
"""使用Vault凭证安全连接设备"""
vault = SecureCredentialManager()
hostname = "SW-Core-01"
creds = vault.get_device_credentials(hostname)
device = Device(
hostname=hostname,
ip="192.168.1.1",
vendor="cisco",
username=creds['username'],
password=creds['password']
)
# 使用凭证执行任务...
result = collect_version_task(device)
return result
6.2 审计日志系统
# audit_logger.py - 操作审计与合规
import json
import hashlib
import hmac
from datetime import datetime
from typing import Dict, Any
class XshellAuditLogger:
"""
记录所有Xshell操作日志,支持SIEM集成
"""
def __init__(self, siem_endpoint: str = None, api_key: str = None):
self.siem_endpoint = siem_endpoint
self.api_key = api_key
self.local_log = "xshell_audit.log"
def log_session_start(self, session_info: Dict):
"""记录会话开始"""
event = {
"event_type": "SESSION_START",
"timestamp": datetime.utcnow().isoformat(),
"session_id": self._generate_session_id(),
"user": session_info.get('username'),
"source_ip": session_info.get('source_ip'),
"target_host": session_info.get('hostname'),
"target_ip": session_info.get('ip'),
"protocol": session_info.get('protocol', 'SSH'),
"auth_method": session_info.get('auth_method', 'password')
}
self._write_log(event)
return event['session_id']
def log_command(self, session_id: str, command: str,
output: str = None, duration_ms: int = 0):
"""记录执行的命令"""
# 敏感命令检测
sensitive_patterns = [
r'password\s+\S+',
r'secret\s+\S+',
r'username\s+\S+\s+password',
r'crypto\s+key',
r'ssh-keygen'
]
is_sensitive = any(re.search(p, command, re.IGNORECASE)
for p in sensitive_patterns)
event = {
"event_type": "COMMAND_EXECUTED",
"timestamp": datetime.utcnow().isoformat(),
"session_id": session_id,
"command_hash": self._hash(command), # 记录哈希而非明文
"command_masked": self._mask_sensitive(command) if is_sensitive else command[:50],
"is_sensitive": is_sensitive,
"output_size": len(output) if output else 0,
"duration_ms": duration_ms,
"risk_score": self._calculate_risk_score(command)
}
self._write_log(event)
# 高风险命令实时告警
if event['risk_score'] > 80:
self._send_alert(f"High risk command detected: {command[:100]}")
def log_session_end(self, session_id: str, duration_seconds: int,
bytes_sent: int = 0, bytes_received: int = 0):
"""记录会话结束"""
event = {
"event_type": "SESSION_END",
"timestamp": datetime.utcnow().isoformat(),
"session_id": session_id,
"duration_seconds": duration_seconds,
"bytes_sent": bytes_sent,
"bytes_received": bytes_received
}
self._write_log(event)
def _generate_session_id(self) -> str:
"""生成唯一会话ID"""
import uuid
return str(uuid.uuid4())
def _hash(self, data: str) -> str:
"""计算SHA256哈希"""
return hashlib.sha256(data.encode()).hexdigest()[:16]
def _mask_sensitive(self, command: str) -> str:
"""脱敏处理"""
# 替换密码等敏感信息
masked = re.sub(r'(password|secret)\s+\S+', r'\1 ***', command, flags=re.IGNORECASE)
return masked
def _calculate_risk_score(self, command: str) -> int:
"""计算命令风险分数(0-100)"""
score = 0
# 危险命令模式
high_risk = ['delete', 'drop', 'format', 'reload', 'write erase', 'rm -rf']
medium_risk = ['configure', 'debug', 'terminal monitor', 'sudo']
cmd_lower = command.lower()
for pattern in high_risk:
if pattern in cmd_lower:
score += 40
for pattern in medium_risk:
if pattern in cmd_lower:
score += 20
# 生产环境标识
if any(x in cmd_lower for x in ['prod', 'production', 'core', 'border']):
score += 15
return min(score, 100)
def _write_log(self, event: Dict):
"""写入日志(本地+远程)"""
# 本地日志
with open(self.local_log, 'a', encoding='utf-8') as f:
f.write(json.dumps(event, ensure_ascii=False) + '\n')
# 发送到SIEM(如果配置了)
if self.siem_endpoint:
self._send_to_siem(event)
def _send_to_siem(self, event: Dict):
"""发送到SIEM系统"""
import requests
try:
headers = {
'Content-Type': 'application/json',
'X-API-Key': self.api_key
}
requests.post(
self.siem_endpoint,
json=event,
headers=headers,
timeout=5
)
except Exception as e:
# SIEM发送失败不阻塞主流程,记录到本地
print(f"SIEM发送失败: {e}")
# 在Xshell脚本中集成审计
logger = XshellAuditLogger(
siem_endpoint="https://siem.company.com/api/events",
api_key="your-api-key"
)
def audited_execute(device: Device, command: str):
"""带审计的命令执行"""
session_id = logger.log_session_start({
'username': device.username,
'hostname': device.hostname,
'ip': device.ip,
'protocol': 'SSH'
})
start_time = time.time()
try:
with XshellSession() as session:
session.connect(device.ip, device.username, device.password)
output = session.send_command(command)
duration = int((time.time() - start_time) * 1000)
logger.log_command(session_id, command, output, duration)
return output
finally:
total_duration = int(time.time() - start_time)
logger.log_session_end(session_id, total_duration)
七、性能优化:异步并发与资源控制
7.1 异步IO与连接池
# async_executor.py - 异步批量执行(使用asyncio和asyncssh)
import asyncio
import asyncssh
from typing import List, Callable
import aiofiles
class AsyncXshellExecutor:
"""
基于asyncio的高性能异步执行器
相比线程池,可支持数千并发连接
"""
def __init__(self, max_concurrent: int = 100,
connect_timeout: int = 10,
command_timeout: int = 60):
self.semaphore = asyncio.Semaphore(max_concurrent)
self.connect_timeout = connect_timeout
self.command_timeout = command_timeout
self.results = []
async def execute_on_device(self, device: Device,
commands: List[str]) -> TaskResult:
"""在单台设备上异步执行命令序列"""
async with self.semaphore: # 限制并发数
try:
async with asyncssh.connect(
device.ip,
username=device.username,
password=device.password,
known_hosts=None, # 生产环境应配置严格主机密钥检查
connect_timeout=self.connect_timeout
) as conn:
outputs = []
for cmd in commands:
result = await asyncio.wait_for(
conn.run(cmd),
timeout=self.command_timeout
)
outputs.append({
'command': cmd,
'stdout': result.stdout,
'stderr': result.stderr,
'exit_code': result.exit_status
})
return TaskResult(
device=device,
success=True,
output=json.dumps(outputs, indent=2)
)
except asyncio.TimeoutError:
return TaskResult(device, False, "", "连接或执行超时")
except asyncssh.Error as e:
return TaskResult(device, False, "", f"SSH错误: {str(e)}")
except Exception as e:
return TaskResult(device, False, "", str(e))
async def execute_batch(self, devices: List[Device],
commands: List[str]) -> List[TaskResult]:
"""批量异步执行"""
tasks = [
self.execute_on_device(dev, commands)
for dev in devices
]
self.results = await asyncio.gather(*tasks, return_exceptions=True)
# 处理异常结果
processed_results = []
for i, result in enumerate(self.results):
if isinstance(result, Exception):
processed_results.append(TaskResult(
device=devices[i],
success=False,
output="",
error_message=str(result)
))
else:
processed_results.append(result)
return processed_results
def run(self, devices: List[Device], commands: List[str]) -> List[TaskResult]:
"""同步接口包装"""
return asyncio.run(self.execute_batch(devices, commands))
# 使用示例:1000台设备并发采集配置
def mass_config_collection():
"""大规模配置采集"""
# 从资产库加载所有设备
db = InventoryDB()
all_devices = db.get_all_active_devices() # 假设有1000台
# 要采集的命令
commands = [
'show running-config',
'show version',
'show interfaces status',
'show ip route summary'
]
# 异步执行(限制100并发,避免网络拥塞)
executor = AsyncXshellExecutor(max_concurrent=100)
results = executor.run(all_devices[:100], commands) # 先测试100台
# 保存结果
for result in results:
if result.success:
filename = f"configs/{result.device.hostname}_{datetime.now():%Y%m%d}.txt"
with open(filename, 'w') as f:
f.write(result.output)
# 生成汇总报告
success_count = sum(1 for r in results if r.success)
print(f"采集完成: {success_count}/{len(results)} 成功")
八、打包部署:插件分发与版本管理
8.1 插件打包结构
XshellPluginPackage/
├── plugin/
│ ├── __init__.py
│ ├── main.py # Xshell入口点
│ ├── config.yaml # 插件配置
│ └── requirements.txt # 依赖列表
├── lib/ # 捆绑的依赖库
│ ├── paramiko/
│ ├── netmiko/
│ └── ...
├── tools/ # 辅助工具
│ ├── install.bat # Windows安装脚本
│ └── install.sh # Linux/Mac安装脚本
├── docs/
│ └── README.md
└── XshellPluginInstaller.exe # 打包的安装程序
8.2 Xshell菜单集成
# main.py - Xshell插件主入口,注册到菜单
import xsh
def initialize_plugin():
"""插件初始化,注册到Xshell菜单"""
# 创建菜单
menu = xsh.Dialog.CreateMenu("网络自动化工具包")
# 添加菜单项
menu.AddItem("批量配置备份", lambda: run_config_backup())
menu.AddItem("合规性检查", lambda: run_compliance_check())
menu.AddItem("密码批量轮换", lambda: run_password_rotation())
menu.AddItem("拓扑自动发现", lambda: run_topology_discovery())
menu.AddItem("实时性能监控", lambda: run_performance_monitor())
menu.AddSeparator()
menu.AddItem("查看审计日志", lambda: view_audit_logs())
menu.AddItem("设置", lambda: open_settings())
# 注册到Xshell工具菜单
xsh.Dialog.RegisterMenu(menu)
def run_config_backup():
"""配置备份功能入口"""
# 选择设备(从会话或资产库)
devices = select_devices_dialog()
if not devices:
return
# 执行备份
executor = BatchExecutor(max_workers=10)
results = executor.execute_on_devices(devices, backup_config_task)
# 显示结果
xsh.Dialog.Message(f"备份完成: {sum(r.success for r in results)}/{len(results)}")
# 打开报告
executor.generate_report("config_backup_report.json")
xsh.Dialog.OpenFile("config_backup_report.json")
def select_devices_dialog() -> List[Device]:
"""设备选择对话框"""
# 从Xshell会话读取
sessions = []
for session in xsh.Sessions:
sessions.append({
'name': session.Name,
'host': session.Host,
'protocol': session.Protocol
})
# 显示选择对话框(多选)
selected = xsh.Dialog.ShowCheckList("选择要操作的设备", sessions)
# 转换为Device对象
devices = []
for sel in selected:
# 从Vault或本地获取凭证
creds = get_credentials(sel['name'])
devices.append(Device(
hostname=sel['name'],
ip=sel['host'],
vendor='unknown', # 可自动检测
username=creds['username'],
password=creds['password']
))
return devices
# 插件加载时自动初始化
if __name__ == "__xshell_plugin__":
initialize_plugin()
九、实战案例:数据中心网络巡检系统
9.1 完整巡检流程
# datacenter_inspection.py - 数据中心巡检系统
class DataCenterInspector:
"""
数据中心网络设备全面巡检系统
"""
def __init__(self):
self.db = InventoryDB()
self.logger = XshellAuditLogger()
self.reporter = InspectionReporter()
def run_full_inspection(self, site: str = None):
"""
执行完整巡检流程
"""
print(f"开始巡检: {site or '所有站点'}")
# 1. 加载设备清单
devices = self.db.get_assets_by_site(site) if site else self.db.get_all_assets()
print(f"加载设备: {len(devices)} 台")
# 2. 连通性检查
reachable = self._connectivity_check(devices)
print(f"可达设备: {len(reachable)}/{len(devices)}")
# 3. 信息采集
device_info = self._collect_device_info(reachable)
# 4. 健康检查
health_results = self._health_check(reachable)
# 5. 配置合规检查
compliance_results = self._compliance_check(reachable)
# 6. 生成报告
report = self.reporter.generate(
site=site,
timestamp=datetime.now(),
summary={
'total_devices': len(devices),
'reachable': len(reachable),
'healthy': sum(1 for h in health_results if h['status'] == 'healthy'),
'compliant': sum(1 for c in compliance_results if c['passed'])
},
details={
'device_info': device_info,
'health': health_results,
'compliance': compliance_results
}
)
# 7. 发送邮件通知
self._send_report_email(report)
# 8. 更新资产库状态
self._update_asset_status(health_results, compliance_results)
return report
def _connectivity_check(self, devices: List[Device]) -> List[Device]:
"""ICMP连通性检查"""
reachable = []
for device in devices:
# 使用pythonping或系统ping
from pythonping import ping
try:
response = ping(device.ip, count=2, timeout=2)
if response.success():
reachable.append(device)
self.db.update_last_seen(device.hostname)
except:
pass
return reachable
def _collect_device_info(self, devices: List[Device]) -> List[Dict]:
"""采集设备详细信息"""
commands = ['show version', 'show inventory', 'show environment']
executor = AsyncXshellExecutor(max_concurrent=50)
results = executor.run(devices, commands)
info_list = []
parser = NetworkDataParser()
for result in results:
if result.success:
parsed = parser.parse('cisco', 'version', result.output)
info_list.append({
'hostname': result.device.hostname,
'info': parsed[0] if parsed else {},
'raw_output': result.output[:2000] # 截断存储
})
return info_list
def _health_check(self, devices: List[Device]) -> List[Dict]:
"""健康状态检查"""
health_checks = [
('cpu', 'show processes cpu', self._check_cpu),
('memory', 'show memory statistics', self._check_memory),
('temperature', 'show environment temperature', self._check_temp),
('interfaces', 'show interfaces status', self._check_interfaces)
]
results = []
for device in devices:
device_health = {'hostname': device.hostname, 'checks': {}}
with XshellSession() as session:
if not session.connect(device.ip, device.username, device.password):
device_health['status'] = 'unreachable'
results.append(device_health)
continue
for check_name, cmd, check_func in health_checks:
try:
output = session.send_command(cmd, timeout=30)
status, details = check_func(output)
device_health['checks'][check_name] = {
'status': status,
'details': details
}
except Exception as e:
device_health['checks'][check_name] = {
'status': 'error',
'details': str(e)
}
# 综合状态
if all(c['status'] == 'ok' for c in device_health['checks'].values()):
device_health['status'] = 'healthy'
elif any(c['status'] == 'critical' for c in device_health['checks'].values()):
device_health['status'] = 'critical'
else:
device_health['status'] = 'warning'
results.append(device_health)
return results
def _check_cpu(self, output: str) -> Tuple[str, Dict]:
"""解析CPU使用率"""
# Cisco示例:CPU utilization for five seconds: 15%/0%; one minute: 12%; five minutes: 10%
import re
match = re.search(r'five seconds:\s+(\d+)%', output)
if match:
usage = int(match.group(1))
return (
'ok' if usage < 50 else 'warning' if usage < 80 else 'critical',
{'cpu_5s_percent': usage}
)
return 'unknown', {}
def _compliance_check(self, devices: List[Device]) -> List[Dict]:
"""配置合规性检查"""
compliance_rules = [
{
'name': 'SSH版本检查',
'command': 'show ip ssh',
'check': lambda out: 'SSH Enabled - version 2' in out,
'severity': 'high'
},
{
'name': '未使用端口关闭',
'command': 'show interfaces description',
'check': lambda out: 'admin down' in out or 'notconnect' not in out.lower(),
'severity': 'medium'
},
{
'name': 'NTP配置检查',
'command': 'show ntp associations',
'check': lambda out: len(out.strip().split('\n')) > 2,
'severity': 'medium'
}
]
results = []
for device in devices:
device_compliance = {'hostname': device.hostname, 'rules': []}
with XshellSession() as session:
session.connect(device.ip, device.username, device.password)
all_passed = True
for rule in compliance_rules:
output = session.send_command(rule['command'])
passed = rule['check'](output)
all_passed = all_passed and passed
device_compliance['rules'].append({
'name': rule['name'],
'passed': passed,
'severity': rule['severity']
})
device_compliance['passed'] = all_passed
results.append(device_compliance)
return results
# 执行巡检
if __name__ == "__main__":
inspector = DataCenterInspector()
report = inspector.run_full_inspection(site="北京数据中心")
print(f"巡检报告已生成: {report['file_path']}")
结语
Xshell的Python扩展能力将终端工具从"手动操作"提升为"可编程平台"。通过本文介绍的技术栈——从基础的会话自动化到企业级的资产管理和安全审计——你可以构建完全符合自身需求的网络管理工具链。
关键成功要素:
- 分层架构:核心API封装、业务逻辑插件、配置模板分离
- 安全第一:Vault集成、审计日志、敏感操作脱敏
- 性能优化:异步并发、连接池、资源限制
- 可维护性:版本管理、自动部署、完善的日志
在这个网络设备数量爆炸、变更频率加快的时代,自动化不是奢侈品,而是必需品。掌握Xshell插件开发,就是掌握了网络运维的"自定义武器"。
附录:开发资源
- Xshell Python API文档:NetSarang官方文档中心
- TextFSM模板库:https://github.com/networktocode/ntc-templates
- 网络设备MIB库:http://www.circitor.fr/Mibs/Html/
转载自:https://blog.csdn.net/u014727709/article/details/157909537
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐


所有评论(0)