feat(agent): enhance configuration safety and multiline command handling

- Add explicit AAA/password configuration prohibition to safety reminders
- Implement multiline command expansion for banner and similar commands
- Add error handling for devices with missing device_type tags
- Improve logging for configuration errors and multiline expansions
This commit is contained in:
YueGuobin 2026-03-13 22:24:36 +08:00
parent 364ff93783
commit 8d9fce1c12
4 changed files with 119 additions and 12 deletions

View File

@ -165,6 +165,7 @@ vlan 10
# SAFETY REMINDERS
While you have configuration permissions, exercise caution:
- **FORBIDDEN**: AAA/password config (enable secret, username, aaa new-model, service password-encryption, line vty) - Provide guidance only
- Avoid destructive commands (reload, erase, format) unless explicitly requested
- Warn user before making major changes
- Recommend backup for critical configurations

View File

@ -196,6 +196,13 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
)
)
# Expand multiline commands (e.g., banner commands with embedded newlines)
# This converts commands like "banner motd #\nline1\nline2\n#" into
# separate commands: ["banner motd #", "line1", "line2", "#"]
device_configs_list = self._expand_multiline_commands(
device_configs_list
)
# Create a mapping of device names to their configuration commands
device_configs_map = self._configs_map(device_configs_list)
@ -208,6 +215,26 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
logger.error("Failed to prepare device hosts data: %s", e)
return [{"error": str(e)}]
# Check if any devices have errors (e.g., missing device_type tag)
error_devices = {
name: data
for name, data in hosts_data.items()
if "error" in data
}
if error_devices:
logger.error(
"Devices with configuration errors: %s",
list(error_devices.keys())
)
return [
{
"device_name": name,
"status": "failed",
"error": data["error"]
}
for name, data in error_devices.items()
]
# Initialize Nornir
try:
dynamic_nr = self._initialize_nornir(hosts_data)
@ -445,6 +472,60 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
return filtered_list, blocked_commands_map
def _expand_multiline_commands(
self, device_configs_list: list[dict[str, Any]]
) -> list[dict[str, Any]]:
"""
Expand commands that contain embedded newline characters.
Some commands (like banner) are passed as single strings with embedded
newlines. This method splits them into separate commands for proper
execution by netmiko_send_config.
Note: netmiko_send_config automatically handles entering config mode,
so any config mode entry commands (configure terminal, system-view, etc.)
are left as-is and will be safely ignored by the device.
Example:
Input: ["banner motd #\nline1\nline2\n#", "exit"]
Output: ["banner motd #", "line1", "line2", "#", "exit"]
Args:
device_configs_list: List of device configs with config_commands.
Returns:
List of device configs with multiline commands expanded.
"""
expanded_list = []
for device_config in device_configs_list:
device_name = device_config["device_name"]
config_commands = device_config["config_commands"]
expanded_commands = []
for cmd in config_commands:
# Check if command contains embedded newlines
if "\n" in cmd:
# Split by newline and expand
lines = cmd.split("\n")
# Filter out empty lines but keep all content lines
expanded_commands.extend([line for line in lines if line.strip()])
logger.info(
"Device %s: Expanded multiline command (%d lines)",
device_name,
len(lines),
)
else:
# Keep single-line commands as-is
expanded_commands.append(cmd)
# Create updated device config
expanded_config = device_config.copy()
expanded_config["config_commands"] = expanded_commands
expanded_list.append(expanded_config)
return expanded_list
def _configs_map(
self, device_config_list: list[dict[str, Any]]
) -> dict[str, list[str]]:

View File

@ -211,6 +211,26 @@ class ExecuteMultipleDeviceCommands(BaseTool):
logger.error("Failed to prepare device hosts data: %s", e)
return [{"error": str(e)}]
# Check if any devices have errors (e.g., missing device_type tag)
error_devices = {
name: data
for name, data in hosts_data.items()
if "error" in data
}
if error_devices:
logger.error(
"Devices with configuration errors: %s",
list(error_devices.keys())
)
return [
{
"device_name": name,
"status": "failed",
"error": data["error"]
}
for name, data in error_devices.items()
]
# Initialize Nornir
try:
dynamic_nr = self._initialize_nornir(hosts_data)

View File

@ -104,20 +104,25 @@ def get_device_ports_from_topology(
elif tag.startswith("platform:"):
platform = tag.split(":", 1)[1].strip()
# Use defaults if not found in tags
# Return error if device_type not found in tags
# Using a default would cause command execution errors
if device_type is None:
device_type = "cisco_ios_telnet"
logger.debug(
"Device '%s': device_type not found in tags, using default: "
"cisco_ios_telnet",
device_name,
)
else:
logger.debug(
"Device '%s': extracted device_type=%s from tags",
device_name,
device_type,
error_msg = (
f"Device '{device_name}': device_type tag not found. "
f"Please add 'device_type:<type>' tag to this device in GNS3. "
f"Current tags: {tags}"
)
logger.error(error_msg)
hosts_data[device_name] = {
"error": error_msg
}
continue
logger.debug(
"Device '%s': extracted device_type=%s from tags",
device_name,
device_type,
)
if platform is None:
platform = "cisco_ios"