YueGuobin b71b323fd4 feat(agent): improve tiktoken initialization and device registration
- Configure tiktoken cache directory to isolate encoding files
- Add logging and timing for tiktoken initialization process
- Make Huawei CE and Ruijie Telnet device registration idempotent
- Add duplicate registration prevention with global flags
- Add logging for device type registration status
- Update .gitignore to exclude tiktoken cache files
2026-03-15 21:25:14 +08:00

302 lines
9.7 KiB
Python

# SPDX-License-Identifier: GPL-3.0-or-later
#
# GNS3-Copilot - AI-powered Network Lab Assistant for GNS3
#
# This file is part of GNS3-Copilot project.
#
# GNS3-Copilot is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# GNS3-Copilot is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNS3-Copilot. If not, see <https://www.gnu.org/licenses/>.
#
# Copyright (C) 2025 Yue Guobin (岳国宾)
# Author: Yue Guobin (岳国宾)
#
# Project Home: https://github.com/yueguobin/gns3-copilot
#
# mypy: ignore-errors
"""
Custom Netmiko device driver for Ruijie (锐捷) devices in GNS3 emulation.
This module provides enhanced handling for Ruijie network devices,
specifically addressing interactive prompts during configuration.
Key Features:
- Inherits from RuijieOSBase (Netmiko's native Ruijie support)
- Handles interactive prompts (e.g., OSPF router-id [yes/no] confirmation)
- Maintains compatibility with Netmiko's Ruijie implementation
Device Type: gns3_ruijie_telnet
"""
import importlib
import logging
import re
import time
from netmiko.ruijie.ruijie_os import RuijieOSBase
logger = logging.getLogger(__name__)
class RuijieTelnetEnhanced(RuijieOSBase):
"""
Enhanced Ruijie device driver with interactive prompt handling.
Inherits from RuijieOSBase to maintain native Netmiko compatibility,
and adds automatic handling for interactive configuration prompts.
"""
# Interactive command patterns that trigger [yes/no] prompts
# These are commands that commonly require confirmation
INTERACTIVE_PATTERNS = [
re.compile(r'^router-id\s+', re.IGNORECASE), # OSPF router-id
re.compile(r'^erase\s+', re.IGNORECASE), # erase startup-config
re.compile(r'^delete\s+', re.IGNORECASE), # delete files
re.compile(r'^format\s+', re.IGNORECASE), # format filesystem
re.compile(r'^reload\b', re.IGNORECASE), # reload/reboot
re.compile(r'^boot\s+system\s+', re.IGNORECASE), # change boot image
]
def __init__(
self,
*args,
**kwargs,
) -> None:
"""Initialize RuijieTelnetEnhanced connection."""
# Set default_enter for telnet (like Netmiko's RuijieOSTelnet)
default_enter = kwargs.get("default_enter")
if default_enter is None:
kwargs["default_enter"] = "\r\n"
super().__init__(*args, **kwargs)
def _preprocess_interactive_commands(
self, config_commands: list[str]
) -> list[str]:
"""
Preprocess commands to insert 'yes' after interactive commands.
Detects commands that trigger [yes/no] prompts and automatically
inserts 'yes' response after them.
Args:
config_commands: List of configuration commands
Returns:
List of commands with 'yes' inserted after interactive commands
"""
processed = []
for cmd in config_commands:
# Add the original command
processed.append(cmd)
# Check if this command matches any interactive pattern
for pattern in self.INTERACTIVE_PATTERNS:
if pattern.match(cmd.strip()):
logging.info(
"Ruijie device: Detected interactive command '%s', "
"inserting 'yes'",
cmd.strip(),
)
# Insert 'yes' after this command
processed.append("yes")
break
return processed
def send_config_set(
self,
config_commands: str | list[str],
**kwargs,
) -> str:
"""
Send configuration commands with interactive prompt handling.
Hybrid Strategy:
1. Preprocess commands to insert 'yes' after known interactive commands
2. Try batch send (fast)
3. If batch fails, fallback to one-by-one send with real-time detection
Args:
config_commands: Configuration commands to send
**kwargs: Additional arguments (exit_config_mode, read_timeout,
etc.)
Returns:
Output from configuration commands
"""
# Convert string to list
if isinstance(config_commands, str):
config_commands = [config_commands]
# Get parameters with defaults
exit_config_mode = kwargs.get("exit_config_mode", True)
enter_config_mode = kwargs.get("enter_config_mode", True)
read_timeout = kwargs.get("read_timeout", 60)
delay_factor = self.global_delay_factor
output = ""
# Enter config mode if needed
if enter_config_mode:
output += self.config_mode()
# Preprocess: Insert 'yes' after known interactive commands
processed_commands = self._preprocess_interactive_commands(
config_commands
)
# Try batch send first (fast path)
try:
output += self._send_config_batch(
processed_commands, read_timeout, delay_factor
)
except Exception as batch_error:
logging.warning(
"Ruijie device: Batch send failed, "
"falling back to one-by-one: %s",
batch_error,
)
# Fallback to one-by-one send with real-time detection
output += self._send_config_one_by_one(
config_commands, read_timeout, delay_factor
)
# Exit config mode if requested
if exit_config_mode:
output += self.exit_config_mode()
return output
def _send_config_batch(
self, commands: list[str], read_timeout: int, delay_factor: float
) -> str:
"""
Send configuration commands in batch (fast).
This works when all interactive prompts have been pre-processed
with 'yes' responses inserted.
"""
output = ""
# Batch send: Write all commands rapidly
for cmd in commands:
self.write_channel(f"{cmd}{self.RETURN}")
time.sleep(0.01)
# Read all output at once
# Use same default as Netmiko (2.0 seconds) to ensure complete output
output += self.read_channel_timing(
read_timeout=read_timeout, last_read=2.0
)
return output
def _send_config_one_by_one(
self, commands: list[str], read_timeout: int, delay_factor: float
) -> str:
"""
Send commands one-by-one with real-time prompt detection.
This is the fallback method when batch send fails.
"""
output = ""
interactive_patterns = [
r"\[yes/no\]",
r"\[y/n\]",
r"\[Y/N\]",
]
# Send commands ONE BY ONE to detect prompts after each command
for cmd in commands:
# Write the command
self.write_channel(f"{cmd}{self.RETURN}")
time.sleep(0.05)
# Read output after this command
new_output = self.read_channel_timing(
read_timeout=10, last_read=0.5
)
output += new_output
# Check if interactive prompt appeared after this command
for pattern in interactive_patterns:
if re.search(pattern, new_output, re.IGNORECASE):
logging.info(
"Ruijie device: Detected interactive prompt "
"after '%s', sending 'yes'",
cmd,
)
# Send 'yes' to confirm
self.write_channel(f"yes{self.RETURN}")
time.sleep(0.3)
# Read the confirmation response
output += self.read_channel_timing(
read_timeout=30, last_read=0.5
)
break
return output
# Register the custom device type with Netmiko
_registered = False # Flag to prevent duplicate registration
def register_custom_device_type() -> None:
"""
Register the custom RuijieTelnetEnhanced device type with Netmiko.
This function adds 'gns3_ruijie_telnet' to Netmiko's CLASS_MAPPER
and updates the platforms lists.
Note: This function is idempotent - multiple calls will only register once.
"""
global _registered
# Prevent duplicate registration
if _registered:
logger.debug("Ruijie Telnet device type already registered, skipping")
return
sd = importlib.import_module("netmiko.ssh_dispatcher")
# Register in both mappers
sd.CLASS_MAPPER_BASE["gns3_ruijie_telnet"] = RuijieTelnetEnhanced
sd.CLASS_MAPPER["gns3_ruijie_telnet"] = RuijieTelnetEnhanced
# Update platforms lists
sd.platforms = list(sd.CLASS_MAPPER.keys())
sd.platforms.sort()
sd.platforms_base = list(sd.CLASS_MAPPER_BASE.keys())
sd.platforms_base.sort()
sd.telnet_platforms = [x for x in sd.platforms if "telnet" in x]
sd.platforms_str = "\n" + "\n".join(sd.platforms_base)
sd.telnet_platforms_str = "\n" + "\n".join(sd.telnet_platforms)
# Mark as registered
_registered = True
logger.info("Successfully registered Ruijie Telnet device type with Netmiko")
# Auto-register on import
try:
register_custom_device_type()
except Exception as e:
logger = logging.getLogger(__name__)
logger.warning(
f"Failed to register Ruijie device type: {e}",
exc_info=True
)