mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
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:
1 parent
ab88bcd57e
commit
db164e9d41
10 files changed
+1462
-25
No files matched your search
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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_
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user