mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-08 02:59:02 +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
439 lines
14 KiB
Python
439 lines
14 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 VPCS (Virtual PC Simulator) in GNS3.
|
|
|
|
VPCS is a lightweight virtual PC simulator used in GNS3 lab environments
|
|
for testing network connectivity and basic IP configuration.
|
|
|
|
Key Features:
|
|
- No authentication required (direct console access)
|
|
- Simple command-line interface (PC1>, PC2>, etc.)
|
|
- Supports basic PC commands (ip, ping, arp, version, etc.)
|
|
- Telnet-based connection
|
|
|
|
Device Type: gns3_vpcs_telnet
|
|
"""
|
|
|
|
import importlib
|
|
import logging
|
|
import re
|
|
import time
|
|
|
|
from netmiko.base_connection import BaseConnection
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ANSI escape code pattern for stripping terminal formatting codes
|
|
# Matches sequences like: [1m (bold), [0m (reset), [4m (underline), etc.
|
|
ANSI_ESCAPE_PATTERN = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
|
|
|
|
|
|
class VPCSTelnet(BaseConnection):
|
|
"""
|
|
Custom VPCS device driver for GNS3 emulation.
|
|
|
|
VPCS devices are lightweight virtual PCs that simulate basic network
|
|
functionality for lab testing. This driver handles the unique
|
|
characteristics of VPCS:
|
|
- No authentication (no username/password)
|
|
- Simple prompt pattern (PC1>, PC2>, etc.)
|
|
- No configuration modes
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*args,
|
|
**kwargs,
|
|
) -> None:
|
|
"""Initialize VPCS Telnet connection."""
|
|
# Set device type to identify this as a VPCS device
|
|
kwargs.setdefault("device_type", "gns3_vpcs_telnet")
|
|
|
|
# VPCS uses carriage return + line feed for command termination
|
|
# This is critical for VPCS devices to respond to commands
|
|
default_enter = kwargs.get("default_enter")
|
|
if default_enter is None:
|
|
kwargs["default_enter"] = "\r\n"
|
|
|
|
kwargs.setdefault("global_delay_factor", 1.0)
|
|
kwargs.setdefault("fast_cli", False)
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def telnet_login(
|
|
self,
|
|
pri_prompt_terminator: str = r"PC\d+>",
|
|
alt_prompt_terminator: str = r"",
|
|
username_pattern: str = r"(?:user:|username|login|user name)",
|
|
pwd_pattern: str = r"assword",
|
|
delay_factor: float = 1.0,
|
|
max_loops: int = 20,
|
|
) -> str:
|
|
"""
|
|
Telnet login for VPCS devices (no authentication).
|
|
|
|
VPCS devices connect directly to command line without username/password.
|
|
This method initializes the connection and waits for the VPCS prompt.
|
|
|
|
Strategy (matching telnetlib3 implementation):
|
|
1. Send 4 newlines with 0.5s delay between each
|
|
2. Wait for VPCS prompt pattern (PC1>, PC2>, etc.)
|
|
3. Return once prompt is detected
|
|
|
|
Args:
|
|
pri_prompt_terminator: Primary prompt pattern (default: r"PC\\d+>")
|
|
alt_prompt_terminator: Not used for VPCS
|
|
username_pattern: Not used (kept for signature compatibility)
|
|
pwd_pattern: Not used (kept for signature compatibility)
|
|
delay_factor: Delay factor for timing
|
|
max_loops: Maximum wait loops
|
|
|
|
Returns:
|
|
Output from the connection process
|
|
"""
|
|
delay_factor = self.select_delay_factor(delay_factor)
|
|
return_msg = ""
|
|
|
|
# Step 1: Clear buffer - read any existing data
|
|
try:
|
|
initial_data = self.read_channel()
|
|
if initial_data:
|
|
return_msg += initial_data
|
|
except Exception:
|
|
# Ignore errors during initial read
|
|
pass
|
|
|
|
# Step 2: Send 4 newlines with delays (matching telnetlib3 exactly)
|
|
# tn.write(b"\n"); sleep(0.5) - repeated 4 times
|
|
for i in range(4):
|
|
try:
|
|
# Send newline directly as bytes
|
|
self.remote_conn.write(b"\n")
|
|
time.sleep(0.5 * delay_factor)
|
|
|
|
# Read response
|
|
new_output = self.read_channel()
|
|
if new_output:
|
|
return_msg += new_output
|
|
|
|
except Exception as e:
|
|
logger.debug("Error during VPCS connection initialization: %s", e)
|
|
|
|
# Step 3: Wait for VPCS prompt pattern
|
|
try:
|
|
# Read until we see the prompt
|
|
output = self.read_until_pattern(
|
|
pattern=pri_prompt_terminator,
|
|
read_timeout=10
|
|
)
|
|
return_msg += output
|
|
|
|
if re.search(pri_prompt_terminator, return_msg, flags=re.M):
|
|
logger.info("VPCS prompt detected")
|
|
return return_msg
|
|
|
|
except Exception as e:
|
|
logger.debug("Error waiting for VPCS prompt: %s", e)
|
|
|
|
# Step 4: Return what we have (connection might still work)
|
|
logger.warning("VPCS prompt not clearly detected, returning current output")
|
|
return return_msg
|
|
|
|
def session_preparation(self) -> None:
|
|
"""
|
|
Prepare the session after connection is established.
|
|
|
|
VPCS doesn't require special session preparation.
|
|
The telnet_login method already handles initialization with 4 newlines.
|
|
"""
|
|
# No additional preparation needed
|
|
logger.debug("VPCS session preparation completed")
|
|
|
|
def send_command(
|
|
self,
|
|
command_string: str,
|
|
expect_string: str | None = None,
|
|
read_timeout: float | None = None,
|
|
delay_factor: float | None = None,
|
|
max_loops: int | None = None,
|
|
strip_prompt: bool = False,
|
|
strip_command: bool = False,
|
|
normalize: bool = False,
|
|
use_textfsm: bool = False,
|
|
) -> str:
|
|
"""
|
|
Send a command to VPCS and return the output.
|
|
|
|
This method exactly matches the telnetlib3 implementation:
|
|
1. Encode command as ASCII and append newline
|
|
2. Write command+newline as single byte sequence
|
|
3. Sleep for 5 seconds
|
|
4. Wait for prompt pattern and capture output
|
|
5. Return captured output
|
|
|
|
Args:
|
|
command_string: The command to send
|
|
expect_string: Pattern to expect (default: VPCS prompt r"PC\\d+>")
|
|
read_timeout: Timeout for reading output
|
|
delay_factor: Not used (kept for signature compatibility)
|
|
max_loops: Not used (kept for signature compatibility)
|
|
strip_prompt: Remove prompt from output (default: False for debugging)
|
|
strip_command: Remove command from output (default: False for debugging)
|
|
normalize: Not used for VPCS
|
|
use_textfsm: Not used for VPCS
|
|
|
|
Returns:
|
|
Command output from VPCS
|
|
"""
|
|
# Use VPCS prompt as default expect_string
|
|
if expect_string is None:
|
|
expect_string = r"PC\d+>"
|
|
|
|
# CRITICAL: Match telnetlib3 behavior exactly
|
|
# tn.write(command.encode(encoding="ascii") + b"\n")
|
|
cmd_bytes = command_string.encode(encoding="ascii") + b"\n"
|
|
self.remote_conn.write(cmd_bytes)
|
|
|
|
logger.debug("VPCS: Sent command '%s' (%d bytes)", command_string, len(cmd_bytes))
|
|
|
|
# Sleep for 5 seconds (matching telnetlib3: sleep(5))
|
|
time.sleep(5)
|
|
|
|
# Wait for prompt pattern and capture output
|
|
# This matches telnetlib3: tn.expect([rb"PC\d+>"]) + tn.read_very_eager()
|
|
# read_until_pattern returns the output including the matched pattern
|
|
try:
|
|
output = self.read_until_pattern(pattern=expect_string, read_timeout=10)
|
|
logger.debug("VPCS: read_until_pattern returned %d chars", len(output))
|
|
except Exception as e:
|
|
# If pattern not found, try to read whatever is available
|
|
logger.debug("VPCS: Pattern not found, reading available output: %s", e)
|
|
output = self.read_channel()
|
|
logger.debug("VPCS: read_channel returned %d chars", len(output) if output else 0)
|
|
|
|
if isinstance(output, bytes):
|
|
output = output.decode("utf-8", errors="replace")
|
|
|
|
logger.debug("VPCS: Raw output before strip: '%s'", output)
|
|
|
|
# Strip ANSI escape codes from VPCS output
|
|
# VPCS uses terminal formatting codes (bold, underline, etc.)
|
|
# that need to be removed for clean output parsing
|
|
output = self._strip_ansi_codes(output)
|
|
|
|
# Strip command and prompt if requested (currently disabled for debugging)
|
|
if strip_command:
|
|
output = self.strip_command_packets(command_string, output)
|
|
if strip_prompt:
|
|
output = self.strip_prompt(output)
|
|
|
|
logger.debug("VPCS: Final output after strip: '%s'", output)
|
|
|
|
return output
|
|
|
|
def send_command_timing(
|
|
self,
|
|
command_string: str,
|
|
delay_factor: float | None = None,
|
|
max_loops: int | None = None,
|
|
strip_prompt: bool = True,
|
|
strip_command: bool = True,
|
|
normalize: bool = False,
|
|
) -> str:
|
|
"""
|
|
Send command using timing-based delay.
|
|
|
|
For VPCS, timing-based and pattern-based methods are identical,
|
|
both use the telnetlib3 implementation pattern with fixed 5s delay.
|
|
|
|
Args:
|
|
command_string: The command to send
|
|
delay_factor: Not used (fixed 5s delay for VPCS)
|
|
max_loops: Not used (kept for signature compatibility)
|
|
strip_prompt: Remove prompt from output
|
|
strip_command: Remove command from output
|
|
normalize: Not used for VPCS
|
|
|
|
Returns:
|
|
Command output from VPCS
|
|
"""
|
|
# For VPCS, timing and pattern-based methods are identical
|
|
return self.send_command(
|
|
command_string=command_string,
|
|
strip_prompt=strip_prompt,
|
|
strip_command=strip_command,
|
|
)
|
|
|
|
def check_config_mode(
|
|
self, check_string: str = "", pattern: str = "", force_regex: bool = False
|
|
) -> bool:
|
|
"""
|
|
VPCS has no configuration mode.
|
|
|
|
This method always returns False for VPCS devices.
|
|
|
|
Returns:
|
|
False (VPCS has no config mode)
|
|
"""
|
|
return False
|
|
|
|
def _strip_ansi_codes(self, text: str) -> str:
|
|
"""
|
|
Strip ANSI escape codes from VPCS output.
|
|
|
|
VPCS uses terminal formatting codes (bold, underline, colors, etc.)
|
|
that need to be removed for clean output parsing. This method removes
|
|
all ANSI escape sequences from the given text.
|
|
|
|
Examples of codes removed:
|
|
- [1m - Bold text
|
|
- [0m - Reset formatting
|
|
- [4m - Underline text
|
|
- [30-47m - Color codes
|
|
|
|
Args:
|
|
text: Text potentially containing ANSI escape codes
|
|
|
|
Returns:
|
|
Text with all ANSI escape codes removed
|
|
"""
|
|
return ANSI_ESCAPE_PATTERN.sub("", text)
|
|
|
|
def config_mode(
|
|
self, config_command: str = "", pattern: str = "", re_flags: int = 0
|
|
) -> str:
|
|
"""
|
|
VPCS has no configuration mode.
|
|
|
|
This method returns an empty string for VPCS devices.
|
|
|
|
Returns:
|
|
Empty string (no config mode to enter)
|
|
"""
|
|
return ""
|
|
|
|
def exit_config_mode(self, exit_config: str = "", pattern: str = "") -> str:
|
|
"""
|
|
VPCS has no configuration mode.
|
|
|
|
This method returns an empty string for VPCS devices.
|
|
|
|
Returns:
|
|
Empty string (no config mode to exit)
|
|
"""
|
|
return ""
|
|
|
|
def disable_paging(
|
|
self,
|
|
command: str = "",
|
|
delay_factor: float | None = None,
|
|
**kwargs,
|
|
) -> str:
|
|
"""
|
|
VPCS doesn't use paging.
|
|
|
|
This method returns an empty string for VPCS devices.
|
|
|
|
Returns:
|
|
Empty string (no paging to disable)
|
|
"""
|
|
return ""
|
|
|
|
|
|
# Register the custom device type with Netmiko
|
|
_registered = False # Flag to prevent duplicate registration
|
|
|
|
def register_custom_device_type() -> None:
|
|
"""
|
|
Register the custom VPCS Telnet device type with Netmiko.
|
|
|
|
This function adds 'gns3_vpcs_telnet' to Netmiko's CLASS_MAPPER
|
|
and updates the platforms lists.
|
|
|
|
IMPORTANT: This function should be called BEFORE using the VPCS device
|
|
type with Netmiko.
|
|
|
|
Note: This function is idempotent - multiple calls will only register once.
|
|
"""
|
|
global _registered
|
|
|
|
# Prevent duplicate registration
|
|
if _registered:
|
|
logger.debug("VPCS Telnet device type already registered, skipping")
|
|
return
|
|
|
|
# Use importlib to avoid namespace conflicts
|
|
sd = importlib.import_module("netmiko.ssh_dispatcher")
|
|
|
|
# Register the device type in both mappers
|
|
# CLASS_MAPPER_BASE is used for base class definitions
|
|
sd.CLASS_MAPPER_BASE["gns3_vpcs_telnet"] = VPCSTelnet
|
|
|
|
# CLASS_MAPPER is used by ConnectHandler for device type validation
|
|
sd.CLASS_MAPPER["gns3_vpcs_telnet"] = VPCSTelnet
|
|
|
|
# CRITICAL: Update the static platforms lists
|
|
# These lists are computed at module import time and won't
|
|
# automatically update when CLASS_MAPPER is modified.
|
|
# We need to manually rebuild them.
|
|
|
|
# Recalculate platforms list
|
|
sd.platforms = list(sd.CLASS_MAPPER.keys())
|
|
sd.platforms.sort()
|
|
|
|
# Recalculate platforms_base list
|
|
sd.platforms_base = list(sd.CLASS_MAPPER_BASE.keys())
|
|
sd.platforms_base.sort()
|
|
|
|
# Recalculate telnet_platforms list
|
|
sd.telnet_platforms = [x for x in sd.platforms if "telnet" in x]
|
|
|
|
# Rebuild the platform strings used in error messages
|
|
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 VPCS Telnet device type with Netmiko")
|
|
|
|
|
|
# Auto-register on import
|
|
# This ensures the device type is available when the module is imported
|
|
try:
|
|
register_custom_device_type()
|
|
except Exception as e:
|
|
# Log but don't fail on import
|
|
logger.warning(
|
|
"Failed to register VPCS device type: %s",
|
|
e,
|
|
exc_info=True
|
|
)
|