mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-04 09:05:14 +03:00
- 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
302 lines
9.7 KiB
Python
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
|
|
)
|