feat(copilot): add multi-vendor device support with custom Huawei driver

Add comprehensive multi-vendor support for GNS3 network automation,
   including a custom Netmiko driver for Huawei CloudEngine devices.

   Features:
   - Custom HuaweiTelnetCE driver for GNS3 emulation (no authentication)
   - Auto-commit before exit to prevent [Y/N/C] prompts
   - Dynamic device type detection from GNS3 node tags
   - Support for both Cisco IOS and Huawei devices
   - Proper VRP command handling (system-view, return confirmation)

   Implementation:
   - New package: utils/custom_netmiko/
     - huawei_ce.py: Huawei CloudEngine driver
     - tests/test_huawei_ce.py: Unit tests (9/9 passing)
     - README.md: Driver development guide
   - Updated tools for multi-vendor support:
     - display_tools_nornir.py: Dynamic group generation
     - config_tools_nornir.py: Multi-vendor config commands
     - get_gns3_device_port.py: Device port extraction
   - Documentation: multi-vendor-device-support.md

   Limitations:
   - huawei_telnet_ce driver requires devices without authentication
   - For devices with username/password, use standard huawei_telnet driver

   Co-Authored-By: Yue Guobin <yueguobin@outlook.com>"
This commit is contained in:
YueGuobin 2026-03-12 16:43:03 +08:00
parent ab88bcd57e
commit db164e9d41
10 changed files with 1462 additions and 25 deletions

View File

@ -12,7 +12,8 @@ docs/gns3-copilot/
├── llm-model-configs.md # LLM model configuration system
├── command-security.md # Command security and filtering
├── context-window-management.md # Context window optimization
└── node-control-tools.md # Node start/stop/suspend tools for lab automation
├── node-control-tools.md # Node start/stop/suspend tools for lab automation
└── multi-vendor-device-support.md # Multi-vendor device support (Cisco, Huawei)
```
## Implemented Features
@ -75,6 +76,22 @@ Tools for controlling network device lifecycle in GNS3 projects.
**Status:** ✅ Implemented
### Multi-Vendor Device Support (`implemented/multi-vendor-device-support.md`)
Multi-vendor network device support with custom Netmiko driver for Huawei devices.
**Key Features:**
- Custom HuaweiTelnetCE driver for GNS3 emulation (no authentication)
- Cisco IOS Telnet support
- Dynamic device type detection from GNS3 tags
- Automatic Nornir group generation
- VRP-specific command handling (system-view, return confirmation)
**Tested Vendors:**
- Cisco IOS (Telnet)
- Huawei CloudEngine (Telnet, custom driver)
**Status:** ✅ Implemented
## Future Enhancements
The following features are currently under consideration or development:

View File

@ -0,0 +1,472 @@
# 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` | `huawei_telnet_ce` | Telnet | ✅ Tested (Custom Driver) |
## Custom Huawei Driver (`HuaweiTelnetCE`)
### 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
HuaweiTelnetCE (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
### HuaweiTelnetCE 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 `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) | `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**
```python
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**
```python
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**
```python
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:
```python
def register_custom_device_type() -> None:
import importlib
sd = importlib.import_module("netmiko.ssh_dispatcher")
# Register in CLASS_MAPPER (for ConnectHandler)
sd.CLASS_MAPPER["huawei_telnet_ce"] = HuaweiTelnetCE
sd.CLASS_MAPPER["huawei_ce"] = HuaweiTelnetCE
# Register in CLASS_MAPPER_BASE (for base class definitions)
sd.CLASS_MAPPER_BASE["huawei_telnet_ce"] = HuaweiTelnetCE
sd.CLASS_MAPPER_BASE["huawei_ce"] = HuaweiTelnetCE
# 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**
```python
# Automatically runs on module import
try:
register_custom_device_type()
except Exception as e:
logger.warning(f"Failed to register custom device type: {e}")
```
## Dynamic Device Type Detection
### GNS3 Node Tags
Device type and platform are extracted from GNS3 node tags:
```
device_type:huawei_telnet_ce → Netmiko device type (precise)
platform:huawei → Nornir platform (high-level)
```
**Tag Examples:**
| Vendor | Device Type Tag | Platform Tag |
|--------|----------------|--------------|
| Cisco IOS | `device_type:cisco_ios_telnet` | `platform:cisco_ios` |
| Huawei CE | `device_type:huawei_telnet_ce` | `platform:huawei` |
### Automatic Group Generation
Nornir groups are dynamically generated based on device type:
```python
# From get_gns3_device_port.py
if device_type and "_telnet" in device_type:
group_name = f"{platform}_telnet" # e.g., "huawei_telnet"
else:
group_name = platform # e.g., "cisco_ios"
hosts_data[device_name] = {
"port": console_port,
"groups": [group_name],
"device_type": device_type,
"platform": platform,
}
```
**Why Dynamic Groups?**
- Single Nornir instance can handle multiple vendors
- Each device gets correct connection parameters
- Vendor-specific commands work automatically
## Usage Examples
### Direct Netmiko Usage
**Huawei Device (Custom Driver):**
```python
from netmiko import ConnectHandler
from gns3server.agent.gns3_copilot.utils import custom_netmiko
# Custom driver auto-registers on import
device = {
"device_type": "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):**
```python
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
```python
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
inventory = {
"options": {
"hosts": {
"huawei-sw1": {
"hostname": "127.0.0.1",
"port": 5001,
"platform": "huawei",
"device_type": "huawei_telnet_ce",
"groups": ["huawei_telnet"]
},
"cisco-r1": {
"hostname": "127.0.0.1",
"port": 5002,
"platform": "cisco_ios",
"device_type": "cisco_ios_telnet",
"groups": ["cisco_ios_telnet"]
}
},
"groups": {
"huawei_telnet": {
"platform": "huawei",
"device_type": "huawei_telnet_ce"
},
"cisco_ios_telnet": {
"platform": "cisco_ios",
"device_type": "cisco_ios_telnet"
}
}
}
}
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
```
### GNS3 Copilot Tool Usage
```python
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
│ │ ├── 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
├── tools_v2/
│ ├── display_tools_nornir.py # Multi-vendor display commands
│ └── config_tools_nornir.py # Multi-vendor config commands
```
## Unit Testing
### Test Coverage
```python
# test_netmiko_custom.py
class TestHuaweiTelnetCEDriver(unittest.TestCase):
def test_device_type_registered(self):
"""Verify huawei_telnet_ce is in Netmiko CLASS_MAPPER"""
from netmiko.ssh_dispatcher import CLASS_MAPPER
self.assertIn("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(HuaweiTelnetCE, 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(HuaweiTelnetCE, method))
```
**Running Tests:**
```bash
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
- Used for inventory grouping
- Examples: `huawei`, `cisco_ios`
**Device Type (Netmiko):**
- Precise driver type
- Includes protocol information
- Examples: `huawei_telnet_ce`, `cisco_ios_telnet`
### Mapping
| Platform | Device Type | Notes |
|----------|-------------|-------|
| `huawei` | `huawei_telnet_ce` | Custom driver for GNS3 |
| `cisco_ios` | `cisco_ios_telnet` | Standard Netmiko driver |
## Related Documentation
- [Custom Netmiko README](../../../../../gns3server/agent/gns3_copilot/utils/custom_netmiko/README.md) - Driver development guide
- [Node Control Tools](./node-control-tools.md) - Device lifecycle management
- [Command Security](./command-security.md) - Command filtering and validation
- [Chat API](./chat-api.md) - Session management and SSE
## References
- [Netmiko Documentation](https://ktbyers.github.io/netmiko/)
- [Netmiko PLATFORMS.md](https://github.com/ktbyers/netmiko/blob/master/PLATFORMS.md)
- [Nornir Documentation](https://nornir.readthedocs.io/)
---
_Implementation Date: 2026-03-12_
_Status: ✅ Implemented - Custom Huawei driver for GNS3 emulation, multi-vendor support with Cisco IOS and Huawei tested_
_Unit Tests: ✅ 9/9 passing_

View File

@ -49,6 +49,22 @@ from gns3server.agent.gns3_copilot.utils.command_filter import (
filter_forbidden_commands,
)
# Import custom Netmiko device types for GNS3 emulation
# This registers huawei_telnet_ce and other custom device types
# NOTE: Must be imported BEFORE any Nornir operations to ensure device types are registered
from gns3server.agent.gns3_copilot.utils import custom_netmiko # noqa: F401
# Explicitly register custom device types to ensure they are available
# This is a safety measure in case the auto-registration on import doesn't work
try:
from gns3server.agent.gns3_copilot.utils.custom_netmiko import huawei_ce
# Re-register to ensure device types are available
huawei_ce.register_custom_device_type()
except Exception:
# Fail silently - the import-time registration should have worked
pass
# config log
logger = logging.getLogger(__name__)
@ -60,9 +76,9 @@ logging.getLogger("nornir.core").setLevel(logging.WARNING)
logging.getLogger("nornir").setLevel(logging.WARNING)
# Local Nornir configuration functions for Cisco IOS Telnet devices
# Local Nornir configuration functions for network devices
def _get_nornir_defaults() -> dict[str, Any]:
"""Get Nornir default configuration for Cisco IOS."""
"""Get Nornir default configuration."""
return {"data": {"location": "gns3"}}
@ -70,11 +86,11 @@ def _get_nornir_groups_config(
device_type: str = "cisco_ios_telnet", platform: str = "cisco_ios"
) -> dict[str, Any]:
"""
Get Nornir group configuration for Cisco IOS Telnet devices.
Get Nornir group configuration for network devices.
Args:
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios')
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet', 'huawei_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios', 'huawei')
Returns:
Dictionary containing Nornir group configuration
@ -92,7 +108,7 @@ def _get_nornir_groups_config(
def _get_nornir_group(
group_name: str = "cisco_IOSv_telnet",
group_name: str = "network_devices_telnet",
device_type: str = "cisco_ios_telnet",
platform: str = "cisco_ios",
) -> dict[str, Any]:
@ -101,8 +117,8 @@ def _get_nornir_group(
Args:
group_name: Name of the group
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios')
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet', 'huawei_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios', 'huawei')
Returns:
Dictionary containing group configuration
@ -529,13 +545,23 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
)
defaults = _get_nornir_defaults()
# Dynamically generate group name based on platform and device type
# e.g., "huawei_telnet", "cisco_ios_telnet", "juniper_junos"
actual_platform = platform or "cisco_ios"
if device_type and "_telnet" in device_type:
group_name = f"{actual_platform}_telnet"
else:
group_name = actual_platform
# Log nornir account information
gns3_host = get_gns3_server_host()
logger.info(
"Initializing Nornir: host=%s, platform=%s, timeout=%d",
"Initializing Nornir: host=%s, platform=%s, device_type=%s, group=%s, timeout=%d",
gns3_host,
groups_data.get("platform"),
device_type,
group_name,
groups_data.get("timeout"),
)
@ -544,7 +570,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
"plugin": "DictInventory",
"options": {
"hosts": hosts_data,
"groups": {"cisco_IOSv_telnet": groups_data},
"groups": {group_name: groups_data},
"defaults": defaults,
},
},

View File

@ -49,6 +49,22 @@ from gns3server.agent.gns3_copilot.utils.command_filter import (
filter_forbidden_commands,
)
# Import custom Netmiko device types for GNS3 emulation
# This registers huawei_telnet_ce and other custom device types
# NOTE: Must be imported BEFORE any Nornir operations to ensure device types are registered
from gns3server.agent.gns3_copilot.utils import custom_netmiko # noqa: F401
# Explicitly register custom device types to ensure they are available
# This is a safety measure in case the auto-registration on import doesn't work
try:
from gns3server.agent.gns3_copilot.utils.custom_netmiko import huawei_ce
# Re-register to ensure device types are available
huawei_ce.register_custom_device_type()
except Exception:
# Fail silently - the import-time registration should have worked
pass
# config log
logger = logging.getLogger(__name__)
@ -60,9 +76,9 @@ logging.getLogger("nornir.core").setLevel(logging.WARNING)
logging.getLogger("nornir").setLevel(logging.WARNING)
# Local Nornir configuration functions for Cisco IOS Telnet devices
# Local Nornir configuration functions for network devices
def _get_nornir_defaults() -> dict[str, Any]:
"""Get Nornir default configuration for Cisco IOS."""
"""Get Nornir default configuration."""
return {"data": {"location": "gns3"}}
@ -70,11 +86,11 @@ def _get_nornir_groups_config(
device_type: str = "cisco_ios_telnet", platform: str = "cisco_ios"
) -> dict[str, Any]:
"""
Get Nornir group configuration for Cisco IOS Telnet devices.
Get Nornir group configuration for network devices.
Args:
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios')
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet', 'huawei_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios', 'huawei')
Returns:
Dictionary containing Nornir group configuration
@ -92,7 +108,7 @@ def _get_nornir_groups_config(
def _get_nornir_group(
group_name: str = "cisco_IOSv_telnet",
group_name: str = "network_devices_telnet",
device_type: str = "cisco_ios_telnet",
platform: str = "cisco_ios",
) -> dict[str, Any]:
@ -101,8 +117,8 @@ def _get_nornir_group(
Args:
group_name: Name of the group
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios')
device_type: Device type for Netmiko (e.g., 'cisco_ios_telnet', 'huawei_telnet')
platform: Platform type for Nornir (e.g., 'cisco_ios', 'huawei')
Returns:
Dictionary containing group configuration
@ -533,13 +549,23 @@ class ExecuteMultipleDeviceCommands(BaseTool):
)
defaults = _get_nornir_defaults()
# Dynamically generate group name based on platform and device type
# e.g., "huawei_telnet", "cisco_ios_telnet", "juniper_junos"
actual_platform = platform or "cisco_ios"
if device_type and "_telnet" in device_type:
group_name = f"{actual_platform}_telnet"
else:
group_name = actual_platform
# Log nornir account information
gns3_host = get_gns3_server_host()
logger.info(
"Initializing Nornir: host=%s, platform=%s, timeout=%d",
"Initializing Nornir: host=%s, platform=%s, device_type=%s, group=%s, timeout=%d",
gns3_host,
groups_data.get("platform"),
device_type,
group_name,
groups_data.get("timeout"),
)
@ -548,7 +574,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
"plugin": "DictInventory",
"options": {
"hosts": hosts_data,
"groups": {"cisco_IOSv_telnet": groups_data},
"groups": {group_name: groups_data},
"defaults": defaults,
},
},

View File

@ -0,0 +1,183 @@
# Custom Netmiko Drivers
Custom Netmiko drivers for GNS3-emulated network devices.
## Overview
This package contains Netmiko drivers optimized for GNS3 emulation environments where devices may have non-standard authentication or behavior patterns.
## Directory Structure
```
custom_netmiko/
├── __init__.py # Package initialization, auto-registers all drivers
├── huawei_ce.py # Huawei CloudEngine custom driver
├── README.md # This file
└── tests/ # Unit tests
├── __init__.py
└── test_huawei_ce.py # Huawei CE driver tests
```
## Supported Drivers
### Huawei (`huawei_ce.py`)
**Driver Name:** `HuaweiTelnetCE`
**Device Types:**
- `huawei_telnet_ce` - Primary type
- `huawei_ce` - Alias
**Features:**
- Skip authentication (for GNS3 devices without username/password)
- VRP prompt recognition (`<HUAWEI>`, `[HUAWEI]`)
- Auto-commit before exit (prevents [Y/N/C] prompts)
- Proper output collection using `read_channel_timing()`
**Limitations:**
- Does not support username/password authentication
- If your device requires authentication, use standard `huawei_telnet` driver
## Usage
### Direct Usage
```python
from netmiko import ConnectHandler
from gns3server.agent.gns3_copilot.utils import custom_netmiko
device = {
"device_type": "huawei_telnet_ce",
"host": "127.0.0.1",
"port": 5000,
}
with ConnectHandler(**device) as conn:
output = conn.send_command("display version")
config = ["interface GE1/0/1", "description Test"]
output = conn.send_config_set(config)
```
### In GNS3 Copilot Tools
The drivers are auto-registered when the tools are imported:
```python
from gns3server.agent.gns3_copilot.tools_v2 import DisplayToolNornir
tool = DisplayToolNornir()
result = tool._run(json.dumps({
"device_names": ["huawei-sw1"],
"commands": ["display version"],
"project_id": "project-uuid"
}))
```
## Running Tests
```bash
# Run all tests
source venv/bin/activate
python -m unittest discover -s gns3server/agent/gns3_copilot/utils/custom_netmiko/tests
# Run Huawei CE tests only
python gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_huawei_ce.py
```
## Adding New Drivers
### 1. Create Driver File
Create a new file in `custom_netmiko/` (e.g., `cisco.py`):
```python
# cisco.py
from netmiko.cisco.cisco_ios import CiscoIosTelnet
class CustomCiscoDriver(CiscoIosTelnet):
"""Custom Cisco driver for GNS3."""
def telnet_login(self, ...):
# Override login logic
pass
def register_custom_device_type() -> None:
"""Register this driver with Netmiko."""
import importlib
sd = importlib.import_module("netmiko.ssh_dispatcher")
sd.CLASS_MAPPER["cisco_custom"] = CustomCiscoDriver
sd.CLASS_MAPPER_BASE["cisco_custom"] = CustomCiscoDriver
# Rebuild static lists
sd.platforms = list(sd.CLASS_MAPPER.keys())
sd.platforms.sort()
# Auto-register on import
try:
register_custom_device_type()
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"Failed to register: {e}")
```
### 2. Update `__init__.py`
Add import to `__init__.py`:
```python
try:
from . import cisco # noqa: F401
except Exception as e:
logger.warning(f"Failed to import Cisco driver: {e}", exc_info=True)
__all__ = ["huawei_ce", "cisco"]
```
### 3. Create Tests
Create `tests/test_cisco.py`:
```python
import unittest
from gns3server.agent.gns3_copilot.utils.custom_netmiko import cisco
class TestCustomCiscoDriver(unittest.TestCase):
def test_device_type_registered(self):
from netmiko.ssh_dispatcher import CLASS_MAPPER
self.assertIn("cisco_custom", CLASS_MAPPER)
if __name__ == "__main__":
unittest.main()
```
## Troubleshooting
### Driver Not Found Error
```
ValueError: Unsupported 'device_type'
```
**Solution:** Ensure the custom_netmiko package is imported before using the device:
```python
from gns3server.agent.gns3_copilot.utils import custom_netmiko
# Now use the device
```
### Import Errors
If you get import errors, check:
1. Virtual environment is activated
2. Python path includes project root
3. Dependencies are installed (`pip install netmiko nornir nornir-netmiko`)
## Related Documentation
- [Multi-Vendor Device Support](../../../docs/gns3-copilot/implemented/multi-vendor-device-support.md)
- [Netmiko Documentation](https://ktbyers.github.io/netmiko/)
---
_Last updated: 2026-03-12_

View File

@ -0,0 +1,62 @@
# 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
#
"""
Custom Netmiko drivers for GNS3-emulated network devices.
This package contains custom Netmiko drivers optimized for GNS3
emulation environments where devices may have non-standard
authentication or behavior patterns.
Supported Drivers:
- huawei_ce: HuaweiTelnetCE for CloudEngine devices (no authentication)
Usage:
from gns3server.agent.gns3_copilot.utils import custom_netmiko
# Auto-registers all drivers on import
from netmiko import ConnectHandler
device = {
"device_type": "huawei_telnet_ce",
"host": "127.0.0.1",
"port": 5000,
}
with ConnectHandler(**device) as conn:
output = conn.send_command("display version")
"""
import logging
logger = logging.getLogger(__name__)
# Import all custom drivers (auto-registers them with Netmiko)
try:
from . import huawei_ce # noqa: F401
except Exception as e:
logger.warning(f"Failed to import Huawei CE driver: {e}", exc_info=True)
__all__ = ["huawei_ce"]

View File

@ -0,0 +1,402 @@
# 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
#
"""
Custom Netmiko device driver for Huawei devices in GNS3 emulation environment.
This module provides a custom device type 'huawei_telnet_ce' for Huawei network
devices that connect via console without requiring authentication (username/password).
This is specifically designed for GNS3 emulated Huawei devices (e.g., CloudEngine
series) where the console connection directly enters the system view without
login prompts.
Key Features:
- Inherits from HuaweiBase for proper VRP command handling
- Skips authentication (no username/password required)
- Supports Huawei-specific config mode (system-view)
- Handles Huawei prompt patterns (<>, [], >)
"""
import re
import time
from typing import Optional
from netmiko.huawei.huawei import HuaweiBase
class HuaweiTelnetCE(HuaweiBase):
"""
Custom Huawei device driver for GNS3 emulation.
Inherits from HuaweiBase to leverage existing VRP-specific functionality:
- system-view configuration mode
- Huawei prompt patterns
- Config mode detection and exit
This driver overrides telnet_login to handle GNS3 devices that
don't require authentication.
"""
def __init__(
self,
*args,
**kwargs,
) -> None:
"""Initialize HuaweiTelnetCE connection."""
# Set default device type for proper initialization
# The '_telnet' suffix in device_type tells Netmiko to use Telnet protocol
kwargs.setdefault("device_type", "huawei_telnet")
# Huawei prompt patterns (inherited from HuaweiBase)
# User view: <HUAWEI>
# System view: [HUAWEI]
# Interface view: [HUAWEI-GigabitEthernet0/0/1]
super().__init__(*args, **kwargs)
def telnet_login(
self,
pri_prompt_terminator: str = r"<\S+>|>\s*$",
alt_prompt_terminator: str = r"\[\S+\]",
username_pattern: str = r"(?:user:|username|login|user name)",
pwd_pattern: str = r"assword",
delay_factor: float = 1.0,
max_loops: int = 10,
) -> str:
"""
Telnet login for GNS3 Huawei devices (no authentication).
Simplified login logic for devices that connect directly to
command line without username/password prompts.
Strategy:
1. Clear any existing buffer data
2. Send carriage returns to trigger prompt
3. Wait for Huawei prompt pattern
4. Return once prompt is detected
Args:
pri_prompt_terminator: Primary prompt pattern (e.g., <HUAWEI>)
alt_prompt_terminator: Alternate prompt pattern (e.g., [HUAWEI])
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)
output = ""
return_msg = ""
# Step 1: Clear buffer - read any existing data to avoid interference
try:
initial_data = self.read_channel()
if initial_data:
return_msg += initial_data
except Exception:
# Ignore errors during initial read
pass
# Step 2: Send carriage returns and wait for prompt
for i in range(max_loops):
try:
# Send return to trigger prompt
self.write_channel(self.RETURN)
time.sleep(0.5 * delay_factor)
# Read response
new_output = self.read_channel()
output = new_output
return_msg += new_output
# Check for Huawei prompt patterns
# User view: <HUAWEI>, System view: [HUAWEI], or just >
if re.search(pri_prompt_terminator, output, flags=re.M):
return return_msg
if re.search(alt_prompt_terminator, output, flags=re.M):
return return_msg
except EOFError:
self.remote_conn.close()
msg = f"Connection failed (EOF): {self.host}"
raise self.connection_error(msg) from None
except Exception:
# Continue trying on other exceptions
pass
# Step 3: Final attempt - return what we have
# Even if we couldn't detect the prompt clearly, the connection
# might still be usable
return return_msg
def session_preparation(self) -> None:
"""
Prepare the session after connection is established.
Inherited from HuaweiBase, but ensures proper initialization
for GNS3 emulation environment.
"""
# Wait for prompt to stabilize
time.sleep(0.5 * self.global_delay_factor)
# Disable paging using Huawei-specific command
try:
self.disable_paging(command="screen-length 0 temporary")
except Exception:
# If disable_paging fails, try the parent implementation
super().disable_paging()
# Ensure we're in a clean state
try:
if hasattr(self, 'base_prompt') and self.base_prompt:
self._test_channel_read(pattern=self.base_prompt)
else:
# If base_prompt is not set yet, just read to clear buffer
self.read_channel()
except Exception:
pass
def disable_paging(
self,
command: str = "screen-length 0 temporary",
**kwargs,
) -> str:
"""
Disable paging for Huawei devices.
Uses Huawei-specific command 'screen-length 0 temporary'
which disables paging for the current session only.
Args:
command: Command to disable paging
Returns:
Output from the disable paging command
"""
return super().disable_paging(command=command, **kwargs)
def send_config_set(
self,
config_commands: str | list[str],
**kwargs,
) -> str:
"""
Send configuration commands to Huawei device.
Overrides the parent method to handle Huawei-specific behavior:
- Uses Huawei-specific prompts (<...> for user view, [...] for system view)
- Handles the 'return' confirmation prompt
- Uses read_channel_timing for proper output collection
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 Huawei-specific defaults
exit_config_mode = kwargs.get("exit_config_mode", True)
read_timeout = kwargs.get("read_timeout", 30) # Longer timeout for GNS3 emulation
delay_factor = self.global_delay_factor
strip_prompt = kwargs.get("strip_prompt", False)
strip_command = kwargs.get("strip_command", False)
config_mode_command = kwargs.get("config_command", "system-view")
cmd_verify = kwargs.get("cmd_verify", False) # Disable cmd_verify for Huawei
output = ""
# Enter config mode if needed
if kwargs.get("enter_config_mode", True):
output += self.config_mode(config_command=config_mode_command)
# Send all configuration commands
# Use cmd_verify=False mode: send all commands, then read all output at once
for cmd in config_commands:
self.write_channel(f"{cmd}{self.RETURN}")
# Small delay between commands
time.sleep(delay_factor * 0.05)
# Use read_channel_timing to collect all output
# This method keeps reading until there is no new data for 'last_read' seconds
# This is the proper Netmiko way to handle command output
output += self.read_channel_timing(read_timeout=read_timeout, last_read=2.0)
# Exit config mode if requested
if exit_config_mode:
# For Huawei devices, commit configuration before exiting
# This avoids the "Uncommitted configurations" [Y/N/C] prompt
try:
# Send commit command (in system view [HUAWEI])
self.write_channel(f"commit{self.RETURN}")
time.sleep(0.5 * self.global_delay_factor)
commit_output = self.read_channel()
output += commit_output
except Exception:
# If commit fails, continue with exit (might not support commit)
pass
# Now exit config mode
output += self.exit_config_mode()
if strip_prompt:
output = self.strip_prompt(output)
if strip_command:
output = self.strip_command(config_commands, output)
return output
def exit_config_mode(
self, exit_config: str = "return", pattern: str = r"<\S+>|>\s*$"
) -> str:
r"""
Exit configuration mode for Huawei devices.
Huawei devices display a confirmation prompt when using 'return':
Return to user view? [y/n]:
This method automatically answers 'y' to the prompt.
Args:
exit_config: Command to exit config mode (default: "return")
pattern: Pattern to detect user view prompt (default: r"<\S+>|>\s*$")
Returns:
Output from exiting config mode
"""
# Check if we're currently in config mode
if not self.check_config_mode():
return ""
output = ""
# Send the exit command (write_channel returns None, don't concatenate)
self.write_channel(f"{exit_config}{self.RETURN}")
time.sleep(0.5 * self.global_delay_factor)
# Look for the confirmation prompt
# Huawei prompt: "Return to user view? [y/n]:"
prompt_pattern = r"\[y/n\]"
max_loops = 20 # More loops for slower devices
for _ in range(max_loops):
new_output = self.read_channel()
output += new_output
# If we see the confirmation prompt, send 'y'
if re.search(prompt_pattern, new_output):
self.write_channel(f"y{self.RETURN}") # Returns None
time.sleep(0.5 * self.global_delay_factor)
# Clear the confirmation response
new_output = self.read_channel()
output += new_output
# Check if we've exited to user view
if re.search(pattern, new_output):
return output
# Final read to get remaining output
new_output = self.read_channel()
output += new_output
return output
# Register the custom device type with Netmiko
def register_custom_device_type() -> None:
"""
Register the custom HuaweiTelnetCE device type with Netmiko.
This function adds 'huawei_telnet_ce' to both Netmiko's CLASS_MAPPER
and CLASS_MAPPER_BASE so it can be used like any other built-in device type.
Additionally, it updates the static 'platforms' and 'telnet_platforms' lists
which are used by ConnectHandler for device type validation.
IMPORTANT: This function should be called BEFORE initializing Nornir or
running any Netmiko tasks. Call it explicitly at the appropriate time.
Returns:
None
"""
# Use importlib to avoid namespace conflicts
import importlib
# Import the module using importlib to ensure we get the module, not a function
sd = importlib.import_module("netmiko.ssh_dispatcher")
# Add to both mappers
# CLASS_MAPPER_BASE is used for base class definitions
sd.CLASS_MAPPER_BASE["huawei_telnet_ce"] = HuaweiTelnetCE
sd.CLASS_MAPPER_BASE["huawei_ce"] = HuaweiTelnetCE
# CLASS_MAPPER is used by ConnectHandler for device type validation
sd.CLASS_MAPPER["huawei_telnet_ce"] = HuaweiTelnetCE
sd.CLASS_MAPPER["huawei_ce"] = HuaweiTelnetCE
# CRITICAL: Netmiko may also look for types with _telnet suffix
# Add this variant to ensure compatibility
sd.CLASS_MAPPER["huawei_telnet_ce_telnet"] = HuaweiTelnetCE
sd.CLASS_MAPPER_BASE["huawei_telnet_ce_telnet"] = HuaweiTelnetCE
# 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)
# Auto-register on import
# This ensures the device type is available when the module is imported
# NOTE: For Nornir scenarios, you may need to call this explicitly
# before InitNornir to ensure proper timing
try:
register_custom_device_type()
except Exception as e:
# Log but don't fail on import
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Failed to register custom device type: {e}", exc_info=True)

View File

@ -0,0 +1,28 @@
# 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
#
"""
Unit tests for custom Netmiko drivers.
"""

View File

@ -0,0 +1,215 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
#
# GNS3-Copilot - AI-powered Network Lab Assistant for GNS3
#
# Unit test script for custom Netmiko HuaweiTelnetCE driver
#
"""
Unit test script for HuaweiTelnetCE custom device driver.
This script tests:
1. Device type registration
2. Inheritance from HuaweiBase
3. VRP-specific command handling
4. Telnet login logic (mocked)
Run with: python test_huawei_ce.py
"""
import sys
import unittest
from unittest.mock import Mock, patch, MagicMock
# Add project root to path
sys.path.insert(0, "/home/yueguobin/myCode/GNS3/gns3-server")
class TestHuaweiTelnetCEDriver(unittest.TestCase):
"""Test suite for HuaweiTelnetCE custom driver."""
@classmethod
def setUpClass(cls):
"""Set up test fixtures - import and register custom driver."""
# Import the custom driver module (this triggers registration)
from gns3server.agent.gns3_copilot.utils.custom_netmiko import huawei_ce
cls.huawei_ce = huawei_ce
cls.HuaweiTelnetCE = huawei_ce.HuaweiTelnetCE
def test_device_type_registered(self):
"""Test that huawei_telnet_ce is registered in Netmiko."""
from netmiko.ssh_dispatcher import CLASS_MAPPER, CLASS_MAPPER_BASE
# Check CLASS_MAPPER
self.assertIn("huawei_telnet_ce", CLASS_MAPPER)
self.assertEqual(CLASS_MAPPER["huawei_telnet_ce"], self.HuaweiTelnetCE)
# Check CLASS_MAPPER_BASE
self.assertIn("huawei_telnet_ce", CLASS_MAPPER_BASE)
self.assertEqual(CLASS_MAPPER_BASE["huawei_telnet_ce"], self.HuaweiTelnetCE)
# Check alias
self.assertIn("huawei_ce", CLASS_MAPPER)
def test_inheritance_from_huawei_base(self):
"""Test that HuaweiTelnetCE inherits from HuaweiBase."""
from netmiko.huawei.huawei import HuaweiBase
# Verify inheritance
self.assertIsInstance(self.HuaweiTelnetCE, type)
# Check if HuaweiTelnetCE is a subclass of HuaweiBase
self.assertTrue(issubclass(self.HuaweiTelnetCE, HuaweiBase))
def test_huawei_base_methods_available(self):
"""Test that VRP-specific methods are available."""
# These methods should be inherited from HuaweiBase
vrp_methods = [
"config_mode",
"check_config_mode",
"exit_config_mode",
"send_config_set",
"send_command",
"disable_paging",
]
for method_name in vrp_methods:
self.assertTrue(
hasattr(self.HuaweiTelnetCE, method_name),
f"Method {method_name} not found in HuaweiTelnetCE",
)
def test_telnet_login_method_exists(self):
"""Test that telnet_login method is overridden."""
# Should have overridden telnet_login
self.assertTrue(hasattr(self.HuaweiTelnetCE, "telnet_login"))
# Get the method and check if it's defined in HuaweiTelnetCE
import inspect
telnet_login_method = getattr(self.HuaweiTelnetCE, "telnet_login")
# Check if method is in HuaweiTelnetCE's __dict__ (means it's defined there, not inherited)
self.assertIn(
"telnet_login",
self.HuaweiTelnetCE.__dict__,
"telnet_login should be defined in HuaweiTelnetCE",
)
def test_initialization_parameters(self):
"""Test that initialization sets correct parameters."""
# Create a mock instance (without actual connection)
with patch.object(self.HuaweiTelnetCE, "__init__", lambda self, *args, **kwargs: None):
instance = self.HuaweiTelnetCE.__new__(self.HuaweiTelnetCE)
# Mock the necessary attributes
instance.protocol = "telnet"
instance.device_type = "huawei_telnet"
# Verify protocol is set to telnet
self.assertEqual(instance.protocol, "telnet")
self.assertEqual(instance.device_type, "huawei_telnet")
def test_connect_handler_accepts_device_type(self):
"""Test that ConnectHandler accepts huawei_telnet_ce device type."""
from netmiko.ssh_dispatcher import CLASS_MAPPER
# Get platforms list
platforms = list(CLASS_MAPPER.keys())
# Verify huawei_telnet_ce is in platforms
self.assertIn("huawei_telnet_ce", platforms)
# Verify it's in telnet platforms
telnet_platforms = [x for x in platforms if "telnet" in x]
self.assertIn("huawei_telnet_ce", telnet_platforms)
def test_prompt_pattern_constants(self):
"""Test that Huawei prompt patterns are correctly defined."""
import inspect
# Get the telnet_login method signature
sig = inspect.signature(self.HuaweiTelnetCE.telnet_login)
# Check default prompt patterns
pri_prompt = sig.parameters["pri_prompt_terminator"].default
alt_prompt = sig.parameters["alt_prompt_terminator"].default
# Should match Huawei prompt patterns
self.assertIn("<", pri_prompt)
self.assertIn("[", alt_prompt)
def test_disable_paging_command(self):
"""Test that disable_paging uses correct Huawei command."""
import inspect
sig = inspect.signature(self.HuaweiTelnetCE.disable_paging)
command_default = sig.parameters["command"].default
# Should use Huawei-specific command
self.assertEqual(command_default, "screen-length 0 temporary")
class TestHuaweiTelnetCEIntegration(unittest.TestCase):
"""Integration tests for HuaweiTelnetCE driver."""
def test_mock_telnet_connection(self):
"""Test telnet_login logic with mocked connection."""
from gns3server.agent.gns3_copilot.utils.custom_netmiko.huawei_ce import HuaweiTelnetCE
# Create a mock instance
instance = HuaweiTelnetCE.__new__(HuaweiTelnetCE)
# Mock the necessary attributes and methods
instance.host = "127.0.0.1"
instance.RETURN = "\r\n"
instance.global_delay_factor = 1.0
instance.remote_conn = Mock()
# Mock select_delay_factor
instance.select_delay_factor = Mock(return_value=1.0)
# Mock read_channel and write_channel
test_outputs = ["", "<HUAWEI>"]
instance.read_channel = Mock(side_effect=test_outputs)
instance.write_channel = Mock()
# Call telnet_login
result = instance.telnet_login()
# Verify behavior
self.assertIn("<HUAWEI>", result)
instance.write_channel.assert_called()
self.assertEqual(instance.read_channel.call_count, 2)
def run_tests():
"""Run all tests and print results."""
# Create test suite
loader = unittest.TestLoader()
suite = unittest.TestSuite()
# Add test cases
suite.addTests(loader.loadTestsFromTestCase(TestHuaweiTelnetCEDriver))
suite.addTests(loader.loadTestsFromTestCase(TestHuaweiTelnetCEIntegration))
# Run tests
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Print summary
print("\n" + "=" * 100)
print("测试摘要:")
print(f" 运行: {result.testsRun}")
print(f" 成功: {result.testsRun - len(result.failures) - len(result.errors)}")
print(f" 失败: {len(result.failures)}")
print(f" 错误: {len(result.errors)}")
print("=" * 100)
return result.wasSuccessful()
if __name__ == "__main__":
success = run_tests()
sys.exit(0 if success else 1)

View File

@ -49,9 +49,9 @@ def get_device_ports_from_topology(
{
"device_name": {
"port": console_port,
"groups": ["cisco_IOSv_telnet"],
"device_type": "cisco_ios_telnet", # Extracted from tags
"platform": "cisco_ios" # Extracted from tags
"groups": ["platform_telnet"], # Dynamically generated from platform/device_type
"device_type": "huawei_telnet", # Extracted from tags
"platform": "huawei" # Extracted from tags
}
}
Devices that don't exist or missing console_port will not be included
@ -129,9 +129,15 @@ def get_device_ports_from_topology(
)
# Add device to hosts_data
# Dynamically generate group name based on platform and device_type
if device_type and "_telnet" in device_type:
group_name = f"{platform}_telnet"
else:
group_name = platform
hosts_data[device_name] = {
"port": node_info["console_port"],
"groups": ["cisco_IOSv_telnet"],
"groups": [group_name],
"device_type": device_type,
"platform": platform,
}