gns3-server/docs/gns3-copilot/implemented/multi-vendor-device-support.md
YueGuobin e2e2e23cf7 feat(copilot): add VPCS Telnet driver with Netmiko and ANSI code stripping
Implement custom VPCS driver and unified tool architecture:

   - Add VPCSTelnet custom driver (vpcs_telnet.py)
     - No authentication (direct console access like VPCS behavior)
     - Simple prompt pattern matching (PC\d+>)
     - Automatic ANSI escape code stripping for clean output
     - Config mode methods return empty (VPCS has no config modes)

   - Replace vpcs_tools_telnetlib3.py with vpcs_tools_netmiko.py
     - Migrate from telnetlib3 to Netmiko + Nornir architecture
     - Unified tool architecture matching config/display tools
     - Improved code consistency and maintainability

   - Add comprehensive test coverage (test_vpcs_telnet.py)
     - 30 unit tests covering all VPCS driver functionality
     - Tests for ANSI code stripping, telnet_login, send_command
     - Tests for device registration and initialization

   - Update VPCS built-in template (services/templates.py)
     - Add platform:vpcs and device_type:gns3_vpcs_telnet tags
     - Automatic driver selection without manual configuration

   - Update documentation (docs/)
     - multi-vendor-device-support.md: VPCS driver documentation
     - netmiko_devices.md: Add VPCS to supported devices list
     - README.md: Update multi-vendor support description
2026-03-14 14:25:37 +08:00

35 KiB

Multi-Vendor Network Device Support

Overview

GNS3-Copilot supports network devices from multiple vendors through Netmiko and Nornir integration. The system includes a custom Netmiko driver for Huawei devices in GNS3 emulation environments and supports dynamic device type detection.

Supported Vendors

Vendor Platform Device Type Protocol Status
Cisco cisco_ios cisco_ios_telnet Telnet Tested
Huawei huawei gns3_huawei_telnet_ce Telnet Tested (Custom Driver)
Ruijie (锐捷) ruijie_os gns3_ruijie_telnet Telnet Tested (Custom Driver)
VPCS vpcs gns3_vpcs_telnet Telnet Tested (Custom Driver)

Custom VPCS Driver (VPCSTelnet)

Problem Statement

VPCS (Virtual PC Simulator) is a lightweight virtual PC simulator used in GNS3 lab environments. Unlike network devices (routers/switches), VPCS devices:

  1. No authentication - Direct console access without username/password
  2. Simple command interface - No configuration modes
  3. Simple prompt pattern - PC1>, PC2>, etc.

Solution: Lightweight Custom Driver

BaseConnection (Netmiko base class)
    ↓
VPCSTelnet (Custom GNS3 driver)

Why Not Use Standard Telnet Driver?

  • Standard drivers attempt authentication (times out)
  • No support for VPCS-specific prompt patterns (PC\d+>)
  • No need for configuration mode handling

VPCSTelnet Implementation

Location

gns3server/agent/gns3_copilot/utils/custom_netmiko/vpcs_telnet.py

Key Features

1. No Authentication

def telnet_login(self, pri_prompt_terminator=r"PC\d+>", ...):
    # Send returns until VPCS prompt detected
    for i in range(max_loops):
        self.write_channel(self.RETURN)
        output = self.read_channel()

        if re.search(pri_prompt_terminator, output):
            return output  # Success - VPCS prompt detected

2. Simple Prompt Recognition

PC1> ip 10.10.0.12/24 10.10.0.254
PC1> ping 10.10.0.254

3. No Configuration Mode

def check_config_mode(self) -> bool:
    return False  # VPCS has no config mode

def config_mode(self) -> str:
    return ""  # No config mode to enter

def exit_config_mode(self) -> str:
    return ""  # No config mode to exit

4. No Paging

def disable_paging(self) -> str:
    return ""  # VPCS doesn't use paging

VPCS Tool Usage

The VPCS driver is used by the execute_vpcs_commands tool:

from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands

tool = VPCSCommands()
result = tool._run(json.dumps({
    "project_id": "<PROJECT_UUID>",
    "device_configs": [
        {
            "device_name": "PC1",
            "commands": [
                "ip 10.10.0.12/24 10.10.0.254",
                "ping 10.10.0.254"
            ]
        }
    ]
}))

VPCS Built-in Template Configuration

Automatic Tags - No Manual Configuration Required

VPCS nodes created from the built-in template automatically include the necessary tags:

Tag Value Purpose
platform vpcs Platform identification
device_type gns3_vpcs_telnet Netmiko driver selection

Built-in Template Definition:

# gns3server/services/templates.py
{
    "template_id": uuid.uuid5(uuid.NAMESPACE_X500, "vpcs"),
    "template_type": "vpcs",
    "name": "VPCS",
    "default_name_format": "PC{0}",
    "category": "guest",
    "symbol": "vpcs_guest",
    "builtin": True,
    "tags": ["platform:vpcs", "device_type:gns3_vpcs_telnet"],  # ✅ Auto-applied
}

User Benefits:

  • No manual tagging required - Tags are applied automatically when creating VPCS nodes
  • Automatic driver selection - Copilot tools automatically use the correct Netmiko driver
  • Consistent behavior - All VPCS nodes from the built-in template work identically
  • Zero configuration - Users don't need to understand device_type tags

How It Works:

  1. User creates a VPCS node from the built-in "VPCS" template
  2. Node automatically inherits the tags: platform:vpcs and device_type:gns3_vpcs_telnet
  3. Copilot tools read these tags and select the appropriate VPCS Netmiko driver
  4. Commands execute using the VPCS-optimized driver (no authentication, simple prompts)

Supported VPCS Commands

Command Description Example
ip Configure/show IP address ip 10.10.0.12/24 10.10.0.254
ping Test connectivity ping 10.10.0.254
arp Display ARP table arp
show ip Show IP configuration show ip
version Show VPCS version version
save Save configuration save
load Load configuration load

Architecture Benefits

Unified Tool Architecture:

  • Uses Nornir for connection management (same as network device tools)
  • Uses Netmiko for command execution (consistent with other tools)
  • Follows same patterns as config_tools_nornir.py and display_tools_nornir.py
  • Simplified codebase - no need for separate telnetlib3 implementation

Migration from telnetlib3:

Aspect Old (telnetlib3) New (Netmiko + Nornir)
Library telnetlib3 Netmiko
Framework Manual threading Nornir
Code Lines ~490 lines ~580 lines (with better structure)
Consistency Unique implementation Same as other tools
Maintenance Separate code path Unified architecture

Custom Huawei Driver (GNS3HuaweiTelnetCE)

Problem Statement

GNS3-emulated Huawei devices connect via console without requiring authentication. Standard Netmiko drivers attempt username/password authentication, causing connection timeouts.

Standard Driver Behavior:

Telnet Connection → Wait for username prompt → Send username → Wait for password → Send password → Access
                    ^ Times out after 20 seconds

GNS3 Huawei Device:

Telnet Connection → Direct access to command line (no login prompts)
                    <HUAWEI>

Solution: Custom Driver Architecture

BaseConnection (Netmiko base class)
    ↓
CiscoBaseConnection (Cisco-style base class)
    ↓
HuaweiBase (Huawei device base class) ← Inherits VRP support
    ↓
GNS3HuaweiTelnetCE (Custom GNS3 driver) ← Overrides telnet_login only

Why Inherit from HuaweiBase?

  • Built-in VRP (Versatile Routing Platform) command handling
  • Huawei-specific configuration mode (system-view)
  • Huawei prompt patterns (<...>, [...])
  • Huawei paging disable (screen-length 0 temporary)
  • Minimal code changes - only override authentication

GNS3HuaweiTelnetCE Implementation

Location

gns3server/agent/gns3_copilot/utils/custom_netmiko/huawei_ce.py

Package Structure:

custom_netmiko/
├── __init__.py              # Package initialization, auto-registers all drivers
├── huawei_ce.py             # Huawei CloudEngine custom driver
├── README.md                # Driver development guide
└── tests/                   # Unit tests
    ├── __init__.py
    └── test_huawei_ce.py    # Huawei CE driver tests

Key Features

  1. Skip Authentication

    • Directly detect Huawei prompt patterns
    • No username/password prompts
    • Connection ready in < 1 second
  2. VRP Prompt Recognition

    User view:    <HUAWEI>
    System view:  [HUAWEI]
    Interface:    [HUAWEI-GigabitEthernet0/0/1]
    
  3. Automatic Confirmation Handling

    • Detects and responds to [y/n] prompts
    • Example: return command asks "Return to user view? [y/n]:"
    • Automatically sends y to confirm
  4. Proper Output Collection

    • Uses Netmiko's read_channel_timing() for reliable output
    • Waits for command completion (2s no new data = done)
    • 30-second absolute timeout prevents hanging
  5. Auto-Commit Before Exit

    • Automatically sends commit command before exiting config mode
    • Prevents "Uncommitted configurations [Y/N/C]" prompt
    • Ensures configuration changes are saved

Limitations

Authentication Requirement:

  • The gns3_huawei_telnet_ce driver is designed for GNS3 devices without authentication
  • If your Huawei device has been configured with a username/password:
    • Option 1: Use the standard huawei_telnet driver (requires username/password)
    • Option 2: Remove authentication from the device for GNS3 testing
  • The driver does not currently auto-detect authentication requirements

When to Use Each Driver:

Scenario Use Driver Requires Credentials?
GNS3 Huawei (fresh, no auth) gns3_huawei_telnet_ce No
GNS3 Huawei (configured with username/password) huawei_telnet Yes
Real Huawei hardware huawei_telnet Yes

Method Overrides

1. telnet_login - Skip Authentication

def telnet_login(self, pri_prompt_terminator=r"<\S+>|>\s*$",
                alt_prompt_terminator=r"\[\S+\]", ...) -> str:
    # Clear buffer
    self.read_channel()

    # Send returns until prompt detected
    for i in range(max_loops):
        self.write_channel(self.RETURN)
        output = self.read_channel()

        # Check for Huawei prompts
        if re.search(pri_prompt_terminator, output):
            return output  # Success!

    return output  # Best effort

2. send_config_set - Configuration Commands

def send_config_set(self, config_commands, **kwargs) -> str:
    # Enter config mode
    output += self.config_mode(config_command="system-view")

    # Send all commands
    for cmd in config_commands:
        self.write_channel(f"{cmd}{self.RETURN}")
        time.sleep(delay_factor * 0.05)

    # Collect output using Netmiko standard method
    output += self.read_channel_timing(read_timeout=30, last_read=2.0)

    # Auto-commit before exit (prevents [Y/N/C] prompt)
    self.write_channel(f"commit{self.RETURN}")
    time.sleep(0.5 * self.global_delay_factor)
    output += self.read_channel()

    # Exit config mode
    output += self.exit_config_mode()

    return output

3. exit_config_mode - Handle Confirmation

def exit_config_mode(self, exit_config="return", pattern=r"<\S+>|>\s*$") -> str:
    self.write_channel(f"return{self.RETURN}")

    # Look for confirmation prompt
    for _ in range(20):
        new_output = self.read_channel()

        if re.search(r"\[y/n\]", new_output):
            self.write_channel(f"y{self.RETURN}")  # Auto-confirm

        if re.search(pattern, new_output):
            return output  # Back to user view

    return output

Device Type Registration

The custom driver must be registered with Netmiko's global mappings:

def register_custom_device_type() -> None:
    import importlib
    sd = importlib.import_module("netmiko.ssh_dispatcher")

    # Register in CLASS_MAPPER (for ConnectHandler)
    sd.CLASS_MAPPER["gns3_huawei_telnet_ce"] = GNS3HuaweiTelnetCE
    sd.CLASS_MAPPER["huawei_ce"] = GNS3HuaweiTelnetCE

    # Register in CLASS_MAPPER_BASE (for base class definitions)
    sd.CLASS_MAPPER_BASE["gns3_huawei_telnet_ce"] = GNS3HuaweiTelnetCE
    sd.CLASS_MAPPER_BASE["huawei_ce"] = GNS3HuaweiTelnetCE

    # CRITICAL: Rebuild static lists
    sd.platforms = list(sd.CLASS_MAPPER.keys())
    sd.platforms.sort()
    sd.telnet_platforms = [x for x in sd.platforms if "telnet" in x]

Important: Static List Problem

  • ssh_dispatcher.platforms is computed at module import time
  • Modifying CLASS_MAPPER doesn't automatically update platforms
  • Must manually rebuild the list after registration

Auto-Registration

# Automatically runs on module import
try:
    register_custom_device_type()
except Exception as e:
    logger.warning(f"Failed to register custom device type: {e}")

Custom Ruijie Driver (RuijieTelnetEnhanced)

Problem Statement

Ruijie (锐捷) network devices exhibit Cisco-like command syntax but have interactive prompts during configuration that can cause standard Netmiko drivers to fail.

Common Issue - OSPF Router-ID:

Router(config-router)#router-id 10.0.0.1
Change router-id and update OSPF process! [yes/no]:

Standard Netmiko's send_config_set() waits for a prompt pattern, but the [yes/no]: prompt doesn't match the expected config mode pattern, causing:

  1. ReadTimeout: Netmiko times out waiting for the prompt
  2. Command Failure: Subsequent commands are not executed
  3. Device Lockup: Console remains in the waiting state

Solution: Hybrid Strategy with Interactive Prompt Handling

BaseConnection (Netmiko base class)
    ↓
CiscoBaseConnection (Cisco-style base class)
    ↓
RuijieOSBase (Netmiko's Ruijie implementation)
    ↓
RuijieTelnetEnhanced (Custom GNS3 driver) ← Adds interactive prompt handling

Why Hybrid Strategy?

  1. Preprocessing: Automatically insert yes after known interactive commands
  2. Fast Path: Batch send for most commands (2-3 seconds for 13 commands)
  3. Fallback: One-by-one send with real-time detection (reliable but slower)

RuijieTelnetEnhanced Implementation

Location

gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py

Key Features

1. Preprocessing - Known Interactive Commands

INTERACTIVE_PATTERNS = [
    re.compile(r'^router-id\s+', re.IGNORECASE),  # OSPF/EIGRP/BGP 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
]

2. Hybrid Send Strategy

┌─────────────────────────────────────┐
│ Input Configuration Commands        │
└──────────────┬──────────────────────┘
               ↓
┌─────────────────────────────────────┐
│ Step 1: Preprocessing               │
│ - Detect interactive commands       │
│ - Insert 'yes' after them           │
└──────────────┬──────────────────────┘
               ↓
┌─────────────────────────────────────┐
│ Step 2: Try Batch Send (Fast)       │
│ - Write all commands rapidly        │
│ - Read output once                  │
│ - last_read=2.0s (Netmiko standard) │
└──────────────┬──────────────────────┘
               ↓
         Success? ──Yes──→ Return output
               │
               No
               ↓
┌─────────────────────────────────────┐
│ Step 3: Fallback (Slow but Reliable)│
│ - Send commands one-by-one          │
│ - Detect prompts after each command │
│ - Send 'yes' when needed            │
│ - last_read=0.5s per command        │
└─────────────────────────────────────┘

3. Batch Send Performance

  • 13 commands with 1 interactive command
  • Fast path: ~2-3 seconds (includes device processing time)
  • Fallback: ~10 seconds (if batch fails)

4. Real-time Detection (Fallback)

# After each command
new_output = self.read_channel_timing(read_timeout=10, last_read=0.5)

# Check for [yes/no] prompt
if re.search(r"\[yes/no\]", new_output, re.IGNORECASE):
    self.write_channel(f"yes{self.RETURN}")
    output += self.read_channel_timing(read_timeout=30, last_read=0.5)

GNS3 Node Tag Configuration

For Ruijie devices in GNS3:

device_type:gns3_ruijie_telnet
platform:ruijie_os

Example Usage:

from netmiko import ConnectHandler
from gns3server.agent.gns3_copilot.utils import custom_netmiko

device = {
    "device_type": "gns3_ruijie_telnet",
    "host": "127.0.0.1",
    "port": 5000,
}

with ConnectHandler(**device) as conn:
    # These commands include router-id (interactive)
    config = [
        "router ospf 1",
        "router-id 10.0.0.1",  # Triggers [yes/no] prompt
        "network 192.168.1.0 0.0.0.255 area 0",
    ]
    # Automatically handles the [yes/no] prompt
    output = conn.send_config_set(config)

Limitations

Interactive Command Coverage:

  • Covered: router-id, erase, delete, format, reload, boot system
  • Not Covered: Unknown or vendor-specific interactive prompts
  • Fallback: If batch fails, falls back to one-by-one with real-time detection

When to Use Each Driver:

Scenario Use Driver
GNS3 Ruijie (known commands) gns3_ruijie_telnet (batch works)
GNS3 Ruijie (unknown commands) gns3_ruijie_telnet (auto-fallback)
Real Ruijie hardware ruijie_os_telnet (standard)

Dynamic Device Type Detection

GNS3 Node Tags

Device type and platform are extracted from GNS3 node tags:

device_type:gns3_huawei_telnet_ce    → Netmiko device type (precise)
platform:huawei                  → Nornir platform (high-level)

Tag Examples:

Vendor Device Type Tag Platform Tag Template Source
Cisco IOS device_type:cisco_ios_telnet platform:cisco_ios User appliance
Huawei CE device_type:gns3_huawei_telnet_ce platform:huawei User appliance
Ruijie device_type:gns3_ruijie_telnet platform:ruijie_os User appliance
VPCS device_type:gns3_vpcs_telnet platform:vpcs Built-in

VPCS Built-in Template:

  • VPCS has a built-in template with pre-configured tags
  • Tags are automatically applied when creating VPCS nodes
  • No manual configuration required - works out of the box
  • Other devices require users to import appliances and configure tags manually

Nornir Best Practice: Host-Level Connection Configuration

The system uses Nornir's configuration priority (host > group > defaults) to handle multi-vendor environments efficiently:

# From get_gns3_device_port.py
hosts_data[device_name] = {
    "port": console_port,
    "platform": platform,  # Reserved for future use (NAPALM, scrapli)
    "groups": ["network_devices"],  # All devices share one group
    "connection_options": {
        "netmiko": {
            "extras": {"device_type": device_type}  # Device-specific driver
        }
    },
}

Why Host-Level connection_options?

  • Each device has its own device_type (host-level config)
  • All devices share common settings via group inheritance (hostname, timeout)
  • No need to dynamically create multiple groups for each device type
  • Cleaner code structure - single generic group for all devices
  • Follows Nornir best practice: "configuration proximity"

Configuration Priority:

Host Level (connection_options.device_type)
    ↓ OVERRIDES
Group Level (hostname, timeout, username, password)
    ↓ OVERRIDES
Defaults Level (data.location)

Before (Old Approach - Dynamic Groups):

# Had to create multiple groups dynamically
groups = {
    "cisco_ios_telnet": {"device_type": "cisco_ios_telnet", ...},
    "huawei_telnet": {"device_type": "gns3_huawei_telnet_ce", ...},
    "juniper_junos": {"device_type": "juniper_junos_telnet", ...},
}
# Each host assigned to its vendor-specific group

After (Current Approach - Host-Level Config):

# Single group for shared settings
groups = {
    "network_devices": {
        "hostname": "127.0.0.1",
        "timeout": 120,
        "username": "",
        "password": "",
    }
}
# Each host has device-specific connection_options
# Host config overrides group config automatically

Architecture Evolution

Problem: Multi-Vendor Device Support

Initial Challenge:

Topology: Cisco R1 + Huawei SW1 + Juniper SRX
    ↓
Need: Different Netmiko drivers for each device
    ↓
Question: How to configure Nornir for multiple device types?

Solution Evolution

Approach 1: Single Group with First Device's Type (Initial Implementation)

# PROBLEM: Only uses first device's configuration
def _initialize_nornir(hosts_data):
    first_device = next(iter(hosts_data.values()))
    device_type = first_device["device_type"]  # Only one type!

    return InitNornir(
        inventory={
            "options": {
                "hosts": hosts_data,  # Has multiple device types
                "groups": {
                    "network_devices": {
                        "connection_options": {
                            "netmiko": {"extras": {"device_type": device_type}}
                        }
                    }
                }
            }
        }
    )

Issue: All devices use the first device's driver!

  • Cisco R1 → Uses Huawei driver (if Huawei is first)
  • Huawei SW1 → Uses Cisco driver (if Cisco is first)

Approach 2: Dynamic Groups (Intermediate Solution)

# COMPLEX: Create multiple groups dynamically
groups = {}
for host_data in hosts_data.values():
    device_type = host_data["device_type"]
    platform = host_data["platform"]
    group_name = f"{platform}_telnet"  # e.g., "huawei_telnet"

    if group_name not in groups:
        groups[group_name] = {
            "platform": platform,
            "connection_options": {
                "netmiko": {"extras": {"device_type": device_type}}
            }
        }

    host_data["groups"] = [group_name]

Issues:

  • Complex logic to detect and create groups
  • Code duplication in multiple files
  • Had to delete helper functions (_get_nornir_groups_config, _get_nornir_group)
  • Not following Nornir best practices

Approach 3: Host-Level Configuration (Current - Best Practice)

# SIMPLE: Single group + host-level device_type
hosts_data[device_name] = {
    "port": console_port,
    "platform": platform,  # Reserved for future use
    "groups": ["network_devices"],  # All devices in one group
    "connection_options": {  # Device-specific config
        "netmiko": {
            "extras": {"device_type": device_type}
        }
    }
}

# Single generic group for shared settings
groups = {
    "network_devices": {
        "hostname": "127.0.0.1",
        "timeout": 120,
        "username": "",
        "password": "",
    }
}

Advantages:

  • Clean, simple code
  • Follows Nornir best practice (host > group > defaults)
  • No dynamic group creation logic
  • Each device's connection_options overrides group settings automatically
  • Easy to extend with new device types

Configuration Priority Demonstration

# Host level (highest priority)
host["connection_options"]["netmiko"]["extras"]["device_type"] = "gns3_huawei_telnet_ce"

     OVERRIDES

# Group level (middle priority)
group["hostname"] = "127.0.0.1"
group["timeout"] = 120

     OVERRIDES

# Defaults level (lowest priority)
defaults["data"]["location"] = "gns3"

Result:

  • Each device uses its own device_type from host level
  • All devices share hostname, timeout from group level
  • All devices share data.location from defaults level

Usage Examples

Direct Netmiko Usage

Huawei Device (Custom Driver):

from netmiko import ConnectHandler
from gns3server.agent.gns3_copilot.utils import custom_netmiko

# Custom driver auto-registers on import
device = {
    "device_type": "gns3_huawei_telnet_ce",
    "host": "127.0.0.1",
    "port": 5000,
    # No username/password needed!
}

with ConnectHandler(**device) as conn:
    # Execute display command
    output = conn.send_command("display version")

    # Execute configuration commands
    config = [
        "interface GE1/0/1",
        "description Uplink-to-Core",
        "undo shutdown"
    ]
    output = conn.send_config_set(config)

Cisco IOS Device (Standard Driver):

from netmiko import ConnectHandler

device = {
    "device_type": "cisco_ios_telnet",
    "host": "127.0.0.1",
    "port": 5001,
    "username": "cisco",
    "password": "cisco",
}

with ConnectHandler(**device) as conn:
    output = conn.send_command("show version")
    config = ["interface GigabitEthernet0/0", "description Test"]
    output = conn.send_config_set(config)

Nornir Multi-Vendor Automation

from nornir import InitNornir
from gns3server.agent.gns3_copilot.utils import custom_netmiko

# Auto-register custom driver (happens automatically on import)
from gns3server.agent.gns3_copilot.utils.custom_netmiko import huawei_ce
huawei_ce.register_custom_device_type()

# Initialize Nornir with mixed-vendor inventory
# Using host-level connection_options (best practice)
inventory = {
    "plugin": "DictInventory",
    "options": {
        "hosts": {
            "huawei-sw1": {
                "hostname": "127.0.0.1",
                "port": 5001,
                "platform": "huawei",  # Reserved for future use
                "groups": ["network_devices"],
                "connection_options": {
                    "netmiko": {
                        "extras": {"device_type": "gns3_huawei_telnet_ce"}
                    }
                }
            },
            "cisco-r1": {
                "hostname": "127.0.0.1",
                "port": 5002,
                "platform": "cisco_ios",
                "groups": ["network_devices"],
                "connection_options": {
                    "netmiko": {
                        "extras": {"device_type": "cisco_ios_telnet"}
                    }
                }
            }
        },
        "groups": {
            "network_devices": {
                "hostname": "127.0.0.1",  # Shared by all devices
                "timeout": 120,
                "username": "",
                "password": "",
            }
        },
        "defaults": {
            "data": {"location": "gns3"}
        }
    }
}

nr = InitNornir(inventory=inventory)

# Execute commands on all devices (multi-vendor)
result = nr.run(task=send_commands, commands=["display version"])

# Each device gets vendor-specific command handling
# huawei-sw1 uses gns3_huawei_telnet_ce driver
# cisco-r1 uses cisco_ios_telnet driver

GNS3 Copilot Tool Usage

from gns3server.agent.gns3_copilot.tools_v2 import DisplayToolNornir

tool = DisplayToolNornir()
result = tool._run(json.dumps({
    "device_names": ["huawei-sw1", "cisco-r1"],
    "commands": ["display version", "show version"],
    "project_id": "project-uuid"
}))

# Returns:
# {
#   "huawei-sw1": {
#     "display version": "<Huawei output>",
#     "status": "success"
#   },
#   "cisco-r1": {
#     "show version": "<Cisco output>",
#     "status": "success"
#   }
# }

Module Structure

gns3server/agent/gns3_copilot/
├── utils/
│   ├── custom_netmiko/            # Custom Netmiko drivers package
│   │   ├── __init__.py             # Package initialization
│   │   ├── huawei_ce.py            # Huawei CloudEngine driver
│   │   ├── ruijie_telnet.py        # Ruijie enhanced driver
│   │   ├── vpcs_telnet.py          # VPCS simulator driver (NEW)
│   │   ├── README.md               # Driver development guide
│   │   └── tests/                  # Unit tests
│   │       ├── __init__.py
│   │       └── test_huawei_ce.py   # Huawei CE driver tests
│   └── get_gns3_device_port.py     # Device port extraction with host-level config
│       ├── _expand_multiline_commands()   # Expand banner commands
│       └── _error_handling()              # device_type missing errors
├── tools_v2/
│   ├── display_tools_nornir.py     # Multi-vendor display commands
│   │   ├── _get_nornir_defaults()  # Returns default Nornir config
│   │   └── _initialize_nornir()    # Single generic group + host-level device_type
│   ├── config_tools_nornir.py      # Multi-vendor config commands
│   │   ├── _get_nornir_defaults()  # Returns default Nornir config
│   │   └── _initialize_nornir()    # Single generic group + host-level device_type
│   │   ├── _expand_multiline_commands()   # Expand banner commands
│   │   └── _error_handling()              # device_type validation
│   └── vpcs_tools_netmiko.py      # VPCS commands using Nornir + Netmiko (NEW)
│       ├── VPCSCommands           # VPCS tool class
│       └── _initialize_nornir()    # VPCS device inventory setup

Key Architectural Changes (2026-03-14):

  • Removed: vpcs_tools_telnetlib3.py - Replaced with Netmiko implementation
  • Added: vpcs_telnet.py - Custom VPCS driver for Netmiko
  • Added: vpcs_tools_netmiko.py - VPCS tool using Nornir + Netmiko
  • Simplified: Unified tool architecture - all tools use Nornir + Netmiko
  • Updated: Module structure - all tools follow same pattern

Key Architectural Changes (2026-03-13):

  • Removed: _get_nornir_groups_config() - No longer needed
  • Removed: _get_nornir_group() - No longer needed
  • Simplified: _initialize_nornir() - Uses single generic group
  • Updated: get_gns3_device_port.py() - Returns host-level connection_options
  • Added: ruijie_telnet.py - Custom Ruijie driver with interactive prompt handling
  • Added: _expand_multiline_commands() - Auto-expands banner and multi-line commands
  • Added: _error_handling() - Validates device_type tags, returns error if missing

Unit Testing

Test Coverage

# test_netmiko_custom.py

class TestGNS3HuaweiTelnetCEDriver(unittest.TestCase):
    def test_device_type_registered(self):
        """Verify gns3_huawei_telnet_ce is in Netmiko CLASS_MAPPER"""
        from netmiko.ssh_dispatcher import CLASS_MAPPER
        self.assertIn("gns3_huawei_telnet_ce", CLASS_MAPPER)

    def test_inheritance_from_huawei_base(self):
        """Verify inherits from HuaweiBase"""
        from netmiko.huawei.huawei import HuaweiBase
        self.assertTrue(issubclass(GNS3HuaweiTelnetCE, HuaweiBase))

    def test_vrp_methods_available(self):
        """Verify VRP-specific methods are available"""
        methods = ["config_mode", "check_config_mode", "exit_config_mode"]
        for method in methods:
            self.assertTrue(hasattr(GNS3HuaweiTelnetCE, method))

Running Tests:

source venv/bin/activate
python gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_huawei_ce.py

Current Test Status: All 9 tests passing

Platform vs Device Type

Key Concepts

Platform (Nornir):

  • High-level vendor identifier
  • Reserved for future use with plugins like NAPALM, scrapli
  • Used for metadata and logging
  • Examples: huawei, cisco_ios
  • ⚠️ Not used by nornir_netmiko (only device_type matters)

Device Type (Netmiko):

  • Precise driver type for Netmiko connection
  • Includes protocol information
  • Actively used to determine which Netmiko driver class to load
  • Examples: gns3_huawei_telnet_ce, cisco_ios_telnet

Why Keep platform Field?

Purpose Plugin Uses platform?
Connection driver nornir_netmiko No (uses device_type)
Driver selection NAPALM Yes
Driver selection scrapli Yes
Metadata/Logging General Yes (future)

Conclusion: The platform field is kept for:

  1. Future plugin support (NAPALM, scrapli)
  2. Debugging and logging (vendor identification)
  3. Data completeness (industry standard practice)

Mapping

Platform Device Type Netmiko Usage Notes
huawei gns3_huawei_telnet_ce Active Custom driver for GNS3
cisco_ios cisco_ios_telnet Active Standard Netmiko driver

Important: For nornir_netmiko, only device_type in connection_options matters. The platform field is informational only.

References


Implementation Date: 2026-03-12

Last Updated: 2026-03-14 (Added VPCS driver and unified tool architecture)

Status: Implemented - Custom drivers for Huawei, Ruijie, and VPCS; multi-vendor support with Cisco IOS, Huawei, Ruijie, and VPCS tested

Architecture: Nornir best practice - host-level connection_options with single generic group

Unit Tests: 9/9 passing

Changelog:

  • 2026-03-14: Added VPCS support and unified tool architecture
    • Implemented VPCSTelnet custom Netmiko driver for VPCS simulator
    • Replaced vpcs_tools_telnetlib3.py with vpcs_tools_netmiko.py
    • Unified all tools to use Nornir + Netmiko architecture
    • Removed dependency on telnetlib3 for VPCS devices
    • Improved code consistency and maintainability
  • 2026-03-13 (Evening): Added Ruijie enhanced driver and interactive command handling
    • Implemented RuijieTelnetEnhanced with hybrid batch/fallback strategy
    • Added automatic yes insertion for known interactive commands (router-id, erase, etc.)
    • Achieves ~2-3 seconds for 13 commands (vs 10+ seconds for one-by-one)
    • Auto-fallback to real-time detection for unknown interactive prompts
    • Registered gns3_ruijie_telnet device type
  • 2026-03-13 (Afternoon): Enhanced configuration safety and command handling
    • Added AAA/password configuration prohibition in system prompts
    • Implemented multi-line command expansion (_expand_multiline_commands())
    • Added device_type tag validation with error feedback
    • Prevents execution of commands that could lock users out of devices
    • Properly handles banner and other multi-line configuration commands
  • 2026-03-13 (Morning): Refactored to use host-level connection_options instead of dynamic groups
    • Removed _get_nornir_groups_config() and _get_nornir_group() helper functions
    • Simplified _initialize_nornir() to use single generic group
    • Updated get_gns3_device_port.py() to return host-level configuration
    • Reserved platform field for future NAPALM/scrapli plugin support
  • 2026-03-12: Initial implementation with custom Huawei driver