diff --git a/docs/gns3-copilot/todo/ai-prompting-for-config-templates.md b/docs/gns3-copilot/todo/ai-prompting-for-config-templates.md new file mode 100644 index 000000000..a0bee83bf --- /dev/null +++ b/docs/gns3-copilot/todo/ai-prompting-for-config-templates.md @@ -0,0 +1,797 @@ +# AI Prompting for Configuration Templates + +## Overview + +This document provides prompts and examples for training the AI to generate structured configuration data instead of full configuration text. This is critical for the Jinja2 template system to work effectively. + +--- + +## Core System Prompt + +```python +# File: gns3server/agent/gns3_copilot/prompts/config_assistant_prompt.py + +CONFIG_GENERATION_SYSTEM_PROMPT = """ +You are an expert network configuration assistant for GNS3. Your role is to help users configure network devices by generating structured configuration data. + +## CRITICAL RULES + +1. **NEVER** generate full configuration text directly +2. **ALWAYS** output structured data (Python dict/JSON format) +3. The system will render actual configurations using Jinja2 templates +4. Only include parameters that are explicitly mentioned by the user +5. Use correct data types (int for numbers, bool for flags, str for text) + +## How It Works + +``` +User Request → AI (Structured Data) → Template Renderer → Full Config → Device +``` + +You are responsible for the "AI (Structured Data)" step only. + +## Supported Vendors and OS Types + +| Vendor | OS Types | +|---------|----------------------| +| cisco | ios, iosxr, nx-os, asa | +| juniper| junos, srx | +| huawei | vrp | +| arista | eos | +| mikrotik| routeros | + +## Available Features and Their Schemas + +### OSPF Configuration + +```python +{ + "ospf": { + "enabled": bool, # Required: Enable OSPF + "process_id": int (1-65535), # Required: OSPF process ID + "router_id": str ("x.x.x.x"), # Optional: Router ID + "networks": [ # Optional: Network statements + { + "address": str, # Network address + "wildcard": str, # Wildcard mask + "area": int # OSPF area (0-4294967295) + } + ], + "passive_interfaces": [str], # Optional: List of passive interfaces + "auto_cost_reference": int, # Optional: Reference bandwidth in Mbps + "default_information_originate": bool, # Optional: Advertise default route + "default_metric": int, # Optional: Default route metric + "interfaces": [ # Optional: Per-interface config + { + "name": str, # Interface name + "cost": int, # OSPF cost + "area": int, # OSPF area + "hello_interval": int, # Hello interval (seconds) + "dead_interval": int # Dead interval (seconds) + } + ] + } +} +``` + +Example: +```python +{ + "ospf": { + "enabled": True, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + {"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0}, + {"address": "10.0.0.0", "wildcard": "0.255.255.255", "area": 1} + ], + "passive_interfaces": ["GigabitEthernet0/0"] + } +} +``` + +### BGP Configuration + +```python +{ + "bgp": { + "enabled": bool, + "as_number": int (1-65535), + "router_id": str ("x.x.x.x"), + "log_neighbor_changes": bool, + "graceful_restart": bool, + "neighbors": [ + { + "ip": str, + "remote_as": int, + "description": str (optional), + "ebgp_multihop": int (optional), + "next_hop_self": bool, + "remove_private_as": bool, + "route_map_in": str (optional), + "route_map_out": str (optional), + "password": str (optional) + } + ], + "address_families": [ + { + "type": str, # "ipv4", "ipv6", "vpnv4", "vpnv6" + "vrf": str (optional), + "redistribute_connected": bool, + "redistribute_static": bool, + "redistribute_ospf": int (optional), + "networks": [ + {"address": str, "mask": str} + ], + "neighbors": [ + { + "ip": str, + "activate": bool, + "route_map_in": str (optional), + "route_map_out": str (optional), + "soft_reconfiguration_inbound": bool + } + ] + } + ] + } +} +``` + +### Interface Configuration + +```python +{ + "interfaces": [ + { + "name": str, + "description": str (optional), + "ip_address": str (optional), + "subnet_mask": str (optional), + "ipv6_address": str (optional), + "secondary_ips": [ + {"address": str, "mask": str} + ], + "enabled": bool, + "mtu": int (optional), + "bandwidth": int (optional), + "speed": str (optional), + "duplex": str (optional), + "acl_in": str (optional), + "acl_out": str (optional), + "nat_inside": bool, + "nat_outside": bool, + "vlan": int (optional), + "trunk_vlans": str (optional) # e.g., "10,20,30" or "10-50" + } + ] +} +``` + +### VLAN Configuration (Cisco IOS) + +```python +{ + "vlans": [ + { + "id": int (1-4094), + "name": str, + "interfaces": [str] # List of interface names + } + ] +} +``` + +### ACL Configuration (Cisco IOS) + +```python +{ + "acl": { + "number": int (1-99 for standard, 100-199 for extended), + "name": str (optional for named ACLs), + "rules": [ + { + "action": str, # "permit" or "deny" + "protocol": str, # "ip", "tcp", "udp", "icmp", etc. + "source": str, # "any", "host x.x.x.x", "x.x.x.x y.y.y.y" + "destination": str, # Same format as source + "source_port": str (optional), + "destination_port": str (optional) + } + ] + } +} +``` + +### NAT Configuration (Cisco IOS) + +```python +{ + "nat": { + "inside_source": { + "pool": str (optional), + "network": str, + "mask": str, + "interface": str (optional), # Use "interface" keyword + "overload": bool + }, + "outside_source": { + # Similar structure + }, + "static": [ + { + "inside_local": str, + "inside_global": str, + "tcp": bool (optional), + "udp": bool (optional), + "local_port": int (optional), + "global_port": int (optional) + } + ] + } +} +``` + +--- + +## Interaction Examples + +### Example 1: Simple OSPF Configuration + +**User**: "Configure OSPF process 100 on R1 with router-id 1.1.1.1. Include network 192.168.1.0/24 in area 0." + +**AI Response** (should call tool): +```python +render_device_config( + node_id="R1", + vendor="cisco", + os_type="ios", + config_data={ + "ospf": { + "enabled": True, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + {"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0} + ] + } + } +) +``` + +### Example 2: Multi-Area OSPF + +**User**: "Set up OSPF on R2. Process ID 50, router-id 2.2.2.2. Put 10.1.0.0/16 in area 0, 172.16.0.0/16 in area 1, and 192.168.100.0/24 in area 2. Make loopback0 passive." + +**AI Response**: +```python +render_device_config( + node_id="R2", + vendor="cisco", + os_type="ios", + config_data={ + "ospf": { + "enabled": True, + "process_id": 50, + "router_id": "2.2.2.2", + "networks": [ + {"address": "10.1.0.0", "wildcard": "0.0.255.255", "area": 0}, + {"address": "172.16.0.0", "wildcard": "0.0.255.255", "area": 1}, + {"address": "192.168.100.0", "wildcard": "0.0.0.255", "area": 2} + ], + "passive_interfaces": ["Loopback0"] + } + } +) +``` + +### Example 3: BGP Configuration + +**User**: "Configure BGP on router R1 with AS 65001. Set up IBGP with R2 at 10.0.0.2 and EBGP with ISP at 203.0.13.2 (AS 65002). Advertise network 192.168.0.0/16." + +**AI Response**: +```python +render_device_config( + node_id="R1", + vendor="cisco", + os_type="ios", + config_data={ + "bgp": { + "enabled": True, + "as_number": 65001, + "router_id": "1.1.1.1", + "log_neighbor_changes": True, + "neighbors": [ + { + "ip": "10.0.0.2", + "remote_as": 65001, + "description": "IBGP_R2", + "next_hop_self": True + }, + { + "ip": "203.0.13.2", + "remote_as": 65002, + "description": "ISP", + "ebgp_multihop": 2 + } + ], + "address_families": [ + { + "type": "ipv4", + "networks": [ + {"address": "192.168.0.0", "mask": "255.255.0.0"} + ], + "neighbors": [ + {"ip": "10.0.0.2", "activate": True}, + {"ip": "203.0.13.2", "activate": True} + ] + } + ] + } + } +) +``` + +### Example 4: Interface Configuration + +**User**: "Configure GigabitEthernet0/0 with IP 192.168.1.1/24, description 'LAN', enable it. Configure GigabitEthernet0/1 with IP 10.0.0.1/30, description 'WAN'." + +**AI Response**: +```python +render_device_config( + node_id="R1", + vendor="cisco", + os_type="ios", + config_data={ + "interfaces": [ + { + "name": "GigabitEthernet0/0", + "description": "LAN", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "enabled": True + }, + { + "name": "GigabitEthernet0/1", + "description": "WAN", + "ip_address": "10.0.0.1", + "subnet_mask": "255.255.255.252", + "enabled": True + } + ] + } +) +``` + +### Example 5: Multi-Feature Configuration + +**User**: "Configure R1 as follows: hostname HQ-ROUTER, GigabitEthernet0/0 = 192.168.1.1/24 (LAN, NAT inside), GigabitEthernet0/1 = 203.0.13.1/30 (WAN, NAT outside), OSPF process 100 with 192.168.1.0/24 in area 0" + +**AI Response**: +```python +render_device_config( + node_id="R1", + vendor="cisco", + os_type="ios", + config_data={ + "hostname": "HQ-ROUTER", + "interfaces": [ + { + "name": "GigabitEthernet0/0", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "description": "LAN", + "nat_inside": True, + "enabled": True + }, + { + "name": "GigabitEthernet0/1", + "ip_address": "203.0.13.1", + "subnet_mask": "255.255.255.252", + "description": "WAN", + "nat_outside": True, + "enabled": True + } + ], + "ospf": { + "enabled": True, + "process_id": 100, + "networks": [ + {"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0} + ] + } + } +) +``` + +### Example 6: Juniper Configuration + +**User**: "Configure Juniper SRX with OSPF. Router-id 10.1.1.1. Area 0 on interface ge-0/0/0.0." + +**AI Response**: +```python +render_device_config( + node_id="srx1", + vendor="juniper", + os_type="junos", + config_data={ + "ospf": { + "enabled": True, + "router_id": "10.1.1.1", + "areas": [ + { + "area_id": "0.0.0.0", + "interfaces": [ + {"name": "ge-0/0/0.0"} + ] + } + ] + } + } +) +``` + +--- + +## Few-Shot Prompting Examples + +When the AI needs to handle complex requests, provide these examples in the system prompt: + +### Example 1: Parsing Network Notation + +``` +User: "Add network 192.168.1.0/24 to OSPF area 0" +AI Output: {"networks": [{"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0}]} + +User: "Add network 10.0.0.0/8 to OSPF area 1" +AI Output: {"networks": [{"address": "10.0.0.0", "wildcard": "0.255.255.255", "area": 1}]} + +Conversion: CIDR to Wildcard +- /24 → 0.0.0.255 +- /16 → 0.0.255.255 +- /8 → 0.255.255.255 +- /30 → 0.0.0.3 +- /32 → 0.0.0.0 +``` + +### Example 2: Interface Naming Conventions + +``` +User: "Configure interface gig0/0" +AI Output: {"name": "GigabitEthernet0/0"} + +User: "Configure fa0/1" +AI Output: {"name": "FastEthernet0/1"} + +User: "Configure eth0" +AI Output: {"name": "Ethernet0/0"} + +Abbreviation Mapping: +- gi/gig/gigabit → GigabitEthernet +- fa/fast → FastEthernet +- eth → Ethernet +- serial → Serial +- lo/loopback → Loopback +``` + +### Example 3: IP Address with Subnet + +``` +User: "IP 192.168.1.1/24" +AI Output: {"ip_address": "192.168.1.1", "subnet_mask": "255.255.255.0"} + +User: "IP 10.0.0.1 255.255.255.252" +AI Output: {"ip_address": "10.0.0.1", "subnet_mask": "255.255.255.252"} + +User: "IP 2001:db8::1/64" +AI Output: {"ipv6_address": "2001:db8::1/64"} +``` + +--- + +## Error Handling + +### When Information is Missing + +**User**: "Configure OSPF" + +**AI Should Ask**: +``` +"I need more information to configure OSPF. Please provide: +- OSPF process ID +- Router ID (optional) +- Networks to advertise with their areas +- Any passive interfaces (optional)" +``` + +### When Request is Ambiguous + +**User**: "Configure BGP with the ISP" + +**AI Should Ask**: +``` +"I need more details for BGP configuration: +- Your local AS number +- ISP's IP address and AS number +- Networks to advertise (optional) +- Any special settings like route-maps, passwords, etc. (optional)" +``` + +### When Feature is Not Supported + +**User**: "Configure MPLS LDP on this router" + +**AI Response**: +``` +"MPLS LDP configuration is not yet supported through the template system. +Available features: OSPF, BGP, Interface, VLAN, ACL, NAT. +Would you like me to help with any of these?" +``` + +--- + +## Best Practices for AI Responses + +1. **Always use tool calls** - Don't just output the data, call the render_device_config tool + +2. **Validate before calling** - Check that all required fields are present + +3. **Ask for clarification** - If critical information is missing, ask the user + +4. **Be explicit about vendor/OS** - Don't assume, ask if not specified + +5. **Handle edge cases**: + - Multiple interfaces with similar configs + - Conflicting parameters + - Invalid values (out of range) + +6. **Provide context** - Explain what you're about to configure before calling the tool + +--- + +## Tool Definition for LangChain + +```python +from langchain_core.tools import tool +from typing import Dict, Any + +@tool +def render_device_config( + node_id: str, + vendor: str, + os_type: str, + config_data: Dict[str, Any] +) -> str: + """ + Render network device configuration using Jinja2 templates. + + Instead of generating full configuration text, provide structured data + that will be rendered through vendor-specific templates. + + Args: + node_id: GNS3 node identifier (e.g., "node-1", "R1") + vendor: Device vendor - cisco, juniper, huawei, arista, mikrotik + os_type: Operating system type - ios, iosxr, nx-os, junos, vrp, eos, routeros + config_data: Structured configuration data (dict) for the features + + Returns: + Rendered configuration string or error message + + Examples: + >>> config = render_device_config( + ... node_id="R1", + ... vendor="cisco", + ... os_type="ios", + ... config_data={ + ... "ospf": { + ... "enabled": True, + ... "process_id": 100, + ... "router_id": "1.1.1.1", + ... "networks": [ + ... {"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0} + ... ] + ... } + ... } + ... ) + + Supported Features: + - ospf: OSPF routing protocol + - bgp: BGP routing protocol + - interface: Interface configuration + - vlan: VLAN configuration + - acl: Access control lists + - nat: NAT configuration + - rip: RIP routing protocol + - eigrp: EIGRP routing protocol + """ + from gns3server.agent.gns3_copilot.config_renderer import ConfigRenderer + + renderer = ConfigRenderer() + + try: + # Validate data + feature = list(config_data.keys())[0] if len(config_data) == 1 else None + if feature: + renderer.validate_data(f"{vendor}_{feature}", config_data) + + # Render + if len(config_data) == 1: + feature = list(config_data.keys())[0] + config = renderer.render(vendor, os_type, feature, config_data) + else: + config = renderer.render_multi(vendor, os_type, config_data) + + return f"Configuration rendered successfully:\n{config}" + + except Exception as e: + return f"Error: {str(e)}" + + +@tool +def list_available_templates() -> Dict[str, Any]: + """ + List all available configuration templates organized by vendor and OS type. + + Returns: + Dictionary of available templates: + { + "cisco": { + "ios": ["ospf", "bgp", "interface", "vlan", "acl", "nat"], + "nx-os": ["ospf", "bgp", "interface"] + }, + "juniper": { + "junos": ["ospf", "bgp", "interface"] + } + } + + Use this to understand what features are supported for each vendor/OS combination. + """ + from gns3server.agent.gns3_copilot.config_renderer import ConfigRenderer + + renderer = ConfigRenderer() + return renderer.get_available_templates() + + +@tool +def validate_config_data( + vendor: str, + feature: str, + config_data: Dict[str, Any] +) -> Dict[str, Any]: + """ + Validate configuration data against JSON schema before rendering. + + Args: + vendor: Device vendor (cisco, juniper, huawei, etc.) + feature: Feature name (ospf, bgp, interface, etc.) + config_data: Configuration data to validate + + Returns: + Validation result with status and optional error details + + Example: + >>> result = validate_config_data( + ... vendor="cisco", + ... feature="ospf", + ... config_data={"ospf": {"enabled": True, "process_id": 100}} + ... ) + >>> # Returns: {"status": "valid"} + """ + from gns3server.agent.gns3_copilot.config_renderer import ConfigRenderer + + renderer = ConfigRenderer() + + try: + renderer.validate_data(f"{vendor}_{feature}", config_data) + return {"status": "valid", "message": "Configuration data is valid"} + except Exception as e: + return {"status": "invalid", "errors": str(e)} +``` + +--- + +## Complete Agent Integration Example + +```python +# gns3server/agent/gns3_copilot/agent/config_agent.py + +from langchain.agents import create_openai_functions_agent, AgentExecutor +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder + +# Define tools +tools = [ + render_device_config, + list_available_templates, + validate_config_data, + # ... other GNS3 tools +] + +# Create prompt +prompt = ChatPromptTemplate.from_messages([ + ("system", CONFIG_GENERATION_SYSTEM_PROMPT), + MessagesPlaceholder(variable_name="chat_history", optional=True), + ("human", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad"), +]) + +# Create agent +agent = create_openai_functions_agent(llm, tools, prompt) +agent_executor = AgentExecutor( + agent=agent, + tools=tools, + verbose=True, + handle_parsing_errors=True, + max_iterations=5 +) + +# Example usage +async def configure_device(user_message: str): + response = await agent_executor.ainvoke({ + "input": user_message, + "chat_history": [] + }) + return response +``` + +--- + +## Testing the AI Prompts + +Use these test cases to verify the AI generates correct structured data: + +```python +test_cases = [ + { + "input": "Configure OSPF process 100 with network 192.168.1.0/24 in area 0", + "expected_keys": ["ospf"], + "expected_values": { + "ospf.process_id": 100, + "ospf.networks[0].address": "192.168.1.0", + "ospf.networks[0].area": 0 + } + }, + { + "input": "Set up BGP AS 65001, neighbor 10.0.0.2 remote-as 65002", + "expected_keys": ["bgp"], + "expected_values": { + "bgp.as_number": 65001, + "bgp.neighbors[0].ip": "10.0.0.2", + "bgp.neighbors[0].remote_as": 65002 + } + }, + # ... more test cases +] +``` + +--- + +## Continuous Improvement + +1. **Collect user interactions** - Save actual requests and AI responses +2. **Analyze errors** - Find patterns in failed generations +3. **Update prompts** - Refine examples and instructions +4. **Expand schemas** - Add new features as needed +5. **Vendor feedback** - Learn from network engineers + +--- + +## Quick Reference Card + +### What AI Should Do + +| User Says | AI Generates | +|-----------|-------------| +| "OSPF process 100" | `{"ospf": {"enabled": True, "process_id": 100}}` | +| "network 192.168.1.0/24 area 0" | `{"networks": [{"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0}]}` | +| "BGP AS 65001" | `{"bgp": {"enabled": True, "as_number": 65001}}` | +| "interface 192.168.1.1/24" | `{"ip_address": "192.168.1.1", "subnet_mask": "255.255.255.0"}` | + +### What AI Should NOT Do + +| Don't ❌ | Instead ✅ | +|---------|-----------| +| Output "router ospf 100" | Output `{"ospf": {"process_id": 100}}` | +| Guess missing values | Ask user for missing values | +| Assume vendor/OS | Ask or detect from node | +| Mix features in one dict | Separate by feature key | +| Use string for numbers | Use int: `process_id: 100` not `"process_id": "100"` | diff --git a/docs/gns3-copilot/todo/config-templates-implementation-guide.md b/docs/gns3-copilot/todo/config-templates-implementation-guide.md new file mode 100644 index 000000000..b4ffb3139 --- /dev/null +++ b/docs/gns3-copilot/todo/config-templates-implementation-guide.md @@ -0,0 +1,709 @@ +# Configuration Templates Implementation Guide + +## Quick Start Examples + +### Example 1: Configure OSPF on a Cisco Router + +**User Request**: +``` +"Configure OSPF on R1 with process ID 100, router-id 1.1.1.1. +Include network 192.168.1.0/24 in area 0 and 10.0.0.0/8 in area 1. +Make GigabitEthernet0/0 a passive interface." +``` + +**AI Should Generate** (structured JSON): +```json +{ + "ospf": { + "enabled": true, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + { + "address": "192.168.1.0", + "wildcard": "0.0.0.255", + "area": 0 + }, + { + "address": "10.0.0.0", + "wildcard": "0.255.255.255", + "area": 1 + } + ], + "passive_interfaces": ["GigabitEthernet0/0"] + } +} +``` + +**Agent Action**: +```python +# The agent calls the render tool with the structured data +result = render_device_config( + node_id="node-1", + vendor="cisco", + os_type="ios", + config_data=ai_output +) +``` + +**Rendered Configuration**: +```cisco +router ospf 100 + router-id 1.1.1.1 + network 192.168.1.0 mask 0.0.0.255 area 0 + network 10.0.0.0 mask 0.255.255.255 area 1 + passive-interface GigabitEthernet0/0 +! +``` + +--- + +### Example 2: Configure BGP with Multiple Neighbors + +**User Request**: +``` +"Configure BGP on R1 with AS 65001. Set up IBGP with R2 (10.0.0.2, AS 65001) +and EBGP with ISP (203.0.13.2, AS 65002). Advertise network 192.168.0.0/16. +Enable route-map INBOUND-FILTER on R2 inbound." +``` + +**AI Generates**: +```json +{ + "bgp": { + "enabled": true, + "as_number": 65001, + "router_id": "1.1.1.1", + "log_neighbor_changes": true, + "neighbors": [ + { + "ip": "10.0.0.2", + "remote_as": 65001, + "description": "IBGP_Peer_R2", + "next_hop_self": true, + "route_map_in": "INBOUND-FILTER" + }, + { + "ip": "203.0.13.2", + "remote_as": 65002, + "description": "ISP_EBGP", + "ebgp_multihop": 2 + } + ], + "address_families": [ + { + "type": "ipv4", + "networks": [ + { + "address": "192.168.0.0", + "mask": "255.255.0.0" + } + ], + "neighbors": [ + {"ip": "10.0.0.2", "activate": true}, + {"ip": "203.0.13.2", "activate": true} + ] + } + ] + } +} +``` + +**Rendered Configuration**: +```cisco +router bgp 65001 + bgp router-id 1.1.1.1 + bgp log-neighbor-changes + neighbor 10.0.0.2 remote-as 65001 + neighbor 10.0.0.2 description IBGP_Peer_R2 + neighbor 10.0.0.2 next-hop-self + neighbor 10.0.0.2 route-map INBOUND-FILTER in + neighbor 203.0.13.2 remote-as 65002 + neighbor 203.0.13.2 description ISP_EBGP + neighbor 203.0.13.2 ebgp-multihop 2 + address-family ipv4 + network 192.168.0.0 mask 255.255.0.0 + neighbor 10.0.0.2 activate + neighbor 203.0.13.2 activate + exit-address-family +! +``` + +--- + +### Example 3: Configure Interfaces with IP Addresses + +**User Request**: +``` +"Configure GigabitEthernet0/0 with IP 192.168.1.1/24, description 'LAN Network'. +Configure GigabitEthernet0/1 with IP 10.0.0.1/30, description 'WAN Link'. +Both interfaces should be enabled." +``` + +**AI Generates**: +```json +{ + "interfaces": [ + { + "name": "GigabitEthernet0/0", + "description": "LAN Network", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "enabled": true + }, + { + "name": "GigabitEthernet0/1", + "description": "WAN Link", + "ip_address": "10.0.0.1", + "subnet_mask": "255.255.255.252", + "enabled": true + } + ] +} +``` + +**Rendered Configuration**: +```cisco +interface GigabitEthernet0/0 + description LAN Network + ip address 192.168.1.1 255.255.255.0 + no shutdown +! +interface GigabitEthernet0/1 + description WAN Link + ip address 10.0.0.1 255.255.255.252 + no shutdown +! +``` + +--- + +## Advanced Examples + +### Example 4: Multi-Feature Configuration + +**User Request**: +``` +"Configure R1 as follows: +- Hostname: CORE-R1 +- GigabitEthernet0/0: 192.168.1.1/24, LAN, enable NAT inside +- GigabitEthernet0/1: 203.0.13.1/30, WAN, enable NAT outside +- OSPF: process 100, router-id 1.1.1.1, advertise 192.168.1.0/24 in area 0 +- NAT: overload interface GigabitEthernet0/1 for 192.168.1.0/24" +``` + +**AI Generates** (complete configuration): +```json +{ + "hostname": "CORE-R1", + "interfaces": [ + { + "name": "GigabitEthernet0/0", + "description": "LAN", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "nat_inside": true, + "enabled": true + }, + { + "name": "GigabitEthernet0/1", + "description": "WAN", + "ip_address": "203.0.13.1", + "subnet_mask": "255.255.255.252", + "nat_outside": true, + "enabled": true + } + ], + "ospf": { + "enabled": true, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + { + "address": "192.168.1.0", + "wildcard": "0.0.0.255", + "area": 0 + } + ] + }, + "nat": { + "inside_source": { + "pool": "LAN_POOL", + "network": "192.168.1.0", + "mask": "255.255.255.0", + "interface": "GigabitEthernet0/1", + "overload": true + } + } +} +``` + +--- + +### Example 5: Juniper JunOS Configuration + +**User Request**: +``` +"Configure Juniper SRX with OSPF area 0 on interface ge-0/0/0.0 with IP 192.168.1.1/24. +Set router-id to 10.1.1.1." +``` + +**AI Generates**: +```json +{ + "ospf": { + "enabled": true, + "router_id": "10.1.1.1", + "areas": [ + { + "area_id": "0.0.0.0", + "interfaces": [ + { + "name": "ge-0/0/0.0", + "address": "192.168.1.1/24" + } + ] + } + ] + } +} +``` + +**Template**: `config_templates/juniper/junos/ospf.j2` +```jinja2 +{% if ospf.enabled %} +protocols { + ospf { +{% if ospf.router_id %} + router-id {{ ospf.router_id }}; +{% endif %} +{% for area in ospf.areas %} + area {{ area.area_id }} { +{% for iface in area.interfaces %} + interface {{ iface.name }} { +{% if iface.address %} + family inet { + address {{ iface.address }}; + } +{% endif %} + } +{% endfor %} + } +{% endfor %} + } +} +{% endif %} +``` + +**Rendered Configuration**: +```junos +protocols { + ospf { + router-id 10.1.1.1; + area 0.0.0.0 { + interface ge-0/0/0.0 { + family inet { + address 192.168.1.1/24; + } + } + } + } +} +``` + +--- + +## Integration with LangGraph Agent + +### Updated Agent Flow + +```python +from langchain_core.messages import HumanMessage, AIMessage, ToolMessage +from langchain.agents import AgentExecutor, create_openai_functions_agent +from langchain.tools import tool + +@tool +def render_and_apply_config( + node_id: str, + vendor: str, + os_type: str, + config_data: dict +) -> str: + """Render configuration and apply to device""" + from gns3server.agent.gns3_copilot.config_renderer import ConfigRenderer + + renderer = ConfigRenderer() + + # Step 1: Validate + try: + renderer.validate_data(f"{vendor}_config", config_data) + except Exception as e: + return f"Validation failed: {e}" + + # Step 2: Render + try: + if len(config_data) == 1: + feature = list(config_data.keys())[0] + config = renderer.render(vendor, os_type, feature, config_data) + else: + config = renderer.render_multi(vendor, os_type, config_data) + + # Step 3: Apply to device (via telnet/console/SSH) + # result = apply_config_to_node(node_id, config) + return f"Configuration rendered successfully:\n{config}" + + except Exception as e: + return f"Rendering failed: {e}" + + +# Updated agent prompt +SYSTEM_PROMPT = """ +You are a network configuration assistant for GNS3. + +When users ask to configure network devices: +1. Extract the configuration requirements +2. Generate STRUCTURED DATA (JSON/dict), NOT full configuration text +3. Call the render_and_apply_config tool with the structured data +4. The system will render the actual configuration using templates + +Example for OSPF: +- User: "Configure OSPF with process 100, network 192.168.1.0/24 in area 0" +- You should output: {"ospf": {"enabled": true, "process_id": 100, ...}} + +Available vendors: cisco, juniper, huawei, arista +Available OS types: ios, iosxr, nexus, junos, vrp, eos +""" +``` + +--- + +## Template Snippets Library + +### OSPF Interface Templates + +**Cisco IOS**: +```jinja2 +{# ospf.j2 - Cisco IOS OSPF #} +{% if ospf.enabled %} +router ospf {{ ospf.process_id }} +{% if ospf.router_id %} + router-id {{ ospf.router_id }} +{% endif %} +{% for network in ospf.networks %} + network {{ network.address }} mask {{ network.wildcard }} area {{ network.area }} +{% endfor %} +{% for iface in ospf.passive_interfaces %} + passive-interface {{ iface }} +{% endfor %} +! +{% endif %} +``` + +**Juniper JunOS**: +```jinja2 +{# ospf.j2 - Juniper JunOS OSPF #} +{% if ospf.enabled %} +protocols { + ospf { +{% if ospf.router_id %} + router-id {{ ospf.router_id }}; +{% endif %} +{% for area in ospf.areas %} + area {{ area.area_id }} { +{% for iface in area.interfaces %} + interface {{ iface.name }}; +{% endfor %} + } +{% endfor %} + } +} +{% endif %} +``` + +**Huawei VRP**: +```jinja2 +{# ospf.j2 - Huawei VRP OSPF #} +{% if ospf.enabled %} +ospf {{ ospf.process_id }} +{% if ospf.router_id %} + router-id {{ ospf.router_id }} +{% endif %} +{% for area in ospf.areas %} + area {{ area.area_id }} +{% for network in area.networks %} + network {{ network.address }} {{ network.wildcard }} +{% endfor %} +{% endfor %} +{% endif %} +``` + +--- + +## Testing Framework + +### Unit Test for Template Rendering + +```python +# tests/agent/test_config_renderer.py + +import pytest +from gns3server.agent.gns3_copilot.config_renderer import ConfigRenderer + +def test_ospf_cisco_ios(): + """Test OSPF configuration rendering for Cisco IOS""" + renderer = ConfigRenderer() + + data = { + "ospf": { + "enabled": True, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + {"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0} + ] + } + } + + config = renderer.render("cisco", "ios", "ospf", data) + + assert "router ospf 100" in config + assert "router-id 1.1.1.1" in config + assert "network 192.168.1.0 mask 0.0.0.255 area 0" in config + +def test_bgp_cisco_ios(): + """Test BGP configuration rendering for Cisco IOS""" + renderer = ConfigRenderer() + + data = { + "bgp": { + "enabled": True, + "as_number": 65001, + "neighbors": [ + {"ip": "10.0.0.2", "remote_as": 65002} + ], + "address_families": [ + { + "type": "ipv4", + "neighbors": [ + {"ip": "10.0.0.2", "activate": True} + ] + } + ] + } + } + + config = renderer.render("cisco", "ios", "bgp", data) + + assert "router bgp 65001" in config + assert "neighbor 10.0.0.2 remote-as 65002" in config + assert "address-family ipv4" in config + assert "neighbor 10.0.0.2 activate" in config + +def test_interface_cisco_ios(): + """Test interface configuration rendering""" + renderer = ConfigRenderer() + + data = { + "interfaces": [ + { + "name": "GigabitEthernet0/0", + "description": "Test Interface", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "enabled": True + } + ] + } + + config = renderer.render("cisco", "ios", "interface", data) + + assert "interface GigabitEthernet0/0" in config + assert "description Test Interface" in config + assert "ip address 192.168.1.1 255.255.255.0" in config + assert "no shutdown" in config +``` + +--- + +## API Integration + +### New Controller Endpoint + +```python +# gns3server/api/routes/controller/config_templates.py + +from fastapi import APIRouter, Depends +from typing import Dict, Any +from gns3server.agent.gns3_copilot.config_renderer import ConfigRenderer + +router = APIRouter() + +@router.get("/config-templates") +async def list_templates() -> Dict[str, Any]: + """List all available configuration templates""" + renderer = ConfigRenderer() + return renderer.get_available_templates() + +@router.post("/config-templates/render") +async def render_config_template( + vendor: str, + os_type: str, + feature: str, + data: Dict[str, Any] +) -> Dict[str, str]: + """Render a configuration template with provided data""" + renderer = ConfigRenderer() + + try: + config = renderer.render(vendor, os_type, feature, data) + return {"status": "success", "config": config} + except Exception as e: + return {"status": "error", "message": str(e)} + +@router.post("/config-templates/validate") +async def validate_config_data( + schema_name: str, + data: Dict[str, Any] +) -> Dict[str, Any]: + """Validate configuration data against schema""" + renderer = ConfigRenderer() + + try: + is_valid = renderer.validate_data(schema_name, data) + return {"status": "valid"} + except Exception as e: + return {"status": "invalid", "errors": str(e)} +``` + +--- + +## Prompt Engineering for AI + +### System Prompt Template + +```python +CONFIG_GENERATION_PROMPT = """ +You are a network configuration expert. When users request device configurations: + +1. UNDERSTAND the requirements (vendor, OS, features, parameters) +2. GENERATE structured data (dict/JSON), NOT full configuration text +3. CALL the appropriate rendering tool with the structured data + +RULES: +- NEVER output full configuration text directly +- ALWAYS use structured data format +- Include only the parameters that are explicitly mentioned +- Use correct data types (int for numbers, bool for flags) +- Follow the JSON schema for each feature + +VENDORS: cisco, juniper, huawei, arista, mikrotik +OS TYPES: ios, iosxr, nx-os, junos, vrp, eos, routeros + +FEATURES AVAILABLE: +- ospf: process_id, router_id, networks[{address,wildcard,area}] +- bgp: as_number, router_id, neighbors[{ip,remote_as,description,...}] +- interface: name, ip_address, subnet_mask, description, enabled +- vlan: id, name, interfaces[] +- acl: number, rules[{action,protocol,source,destination}] +- nat: inside_source, outside_source, static + +EXAMPLE: + +User: "Configure OSPF process 100 with router-id 1.1.1.1, network 192.168.1.0/24 area 0" + +Your tool call: +render_device_config( + node_id="node-1", + vendor="cisco", + os_type="ios", + config_data={{ + "ospf": {{ + "enabled": True, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + {{"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0}} + ] + }} + }} +) +""" +``` + +--- + +## Migration Path + +### Phase 1: Core Templates (Week 1-2) +- Cisco IOS: ospf, bgp, interface, vlan, acl +- Juniper JunOS: ospf, bgp, interface +- Schema definitions + +### Phase 2: Extended Features (Week 3-4) +- NAT, QoS, Multicast +- Nexus, IOS-XR variants +- Huawei VRP support + +### Phase 3: Advanced Features (Week 5-6) +- MPLS, VPN +- Firewall policies (ASA, SRX) +- Automation and testing + +### Phase 4: Integration (Week 7-8) +- Integrate with AI Copilot +- Add rendering endpoint to API +- Testing and validation + +--- + +## Best Practices + +1. **Template Design**: + - Keep templates simple and focused + - Use conditionals sparingly + - Add comments for complex logic + - Follow vendor syntax conventions + +2. **Schema Design**: + - Define all fields with types + - Add descriptions for AI + - Include validation rules + - Use enums for fixed values + +3. **AI Prompting**: + - Provide clear examples + - Specify expected output format + - Include error handling guidance + - Test with various inputs + +4. **Testing**: + - Unit test each template + - Test with real devices + - Validate schemas + - Integration testing + +--- + +## Troubleshooting + +### Common Issues + +**Issue**: Template not found +``` +Solution: Check template path format: "{vendor}/{os_type}/{feature}.j2" +``` + +**Issue**: Invalid data structure +``` +Solution: Validate against JSON schema first +``` + +**Issue**: Rendering produces empty config +``` +Solution: Check if feature flag "enabled" is set to True +``` + +**Issue**: Syntax error in rendered config +``` +Solution: Review template logic, check conditional statements +``` diff --git a/docs/gns3-copilot/todo/jinja2-config-templates-system.md b/docs/gns3-copilot/todo/jinja2-config-templates-system.md new file mode 100644 index 000000000..68d1b2164 --- /dev/null +++ b/docs/gns3-copilot/todo/jinja2-config-templates-system.md @@ -0,0 +1,1084 @@ +# Jinja2-Based Configuration Template System for GNS3 AI Copilot + +## Overview + +This document outlines a configuration template system using Jinja2 that allows the AI to generate structured data (JSON) instead of full configuration files. The templates handle the rendering of vendor-specific configurations. + +## Architecture + +``` +User Natural Language + │ + ▼ + ┌─────────────┐ + │ AI Agent │ + │ (LLM) │ + └──────┬──────┘ + │ + ▼ + Structured Data (JSON) + │ + ▼ + ┌──────────────────────────────────┐ + │ Jinja2 Configuration Renderer │ + │ ┌────────────────────────────┐ │ + │ │ Template Library │ │ + │ │ ├── cisco/ │ │ + │ │ │ ├── ospf.j2 │ │ + │ │ │ ├── bgp.j2 │ │ + │ │ │ ├── interface.j2 │ │ + │ │ │ └── ... │ │ + │ │ ├── juniper/ │ │ + │ │ ├── huawei/ │ │ + │ │ └── linux/ │ │ + │ └────────────────────────────┘ │ + └──────────────────┬───────────────┘ + │ + ▼ + Full Configuration File + │ + ▼ + Push to Device +``` + +## Directory Structure + +``` +gns3server/agent/gns3_copilot/ +├── config_templates/ +│ ├── README.md # This file +│ ├── base/ # Base/common templates +│ │ ├── interface_common.j2 # Common interface config +│ │ ├── routing_common.j2 # Common routing config +│ │ └── security_common.j2 # Common security config +│ ├── cisco/ # Cisco device templates +│ │ ├── ios/ # IOS/IOS-XE +│ │ │ ├── ospf.j2 +│ │ │ ├── bgp.j2 +│ │ │ ├── interface.j2 +│ │ │ ├── acl.j2 +│ │ │ ├── nat.j2 +│ │ │ ├── vlan.j2 +│ │ │ └── multiservice.j2 +│ │ ├── nexus/ # Nexus switches +│ │ │ ├── ospf.j2 +│ │ │ ├── bgp.j2 +│ │ │ └── interface.j2 +│ │ └── asa/ # ASA firewall +│ │ ├── nat.j2 +│ │ └── access_list.j2 +│ ├── juniper/ # Juniper devices +│ │ ├── junos/ +│ │ │ ├── ospf.j2 +│ │ │ ├── bgp.j2 +│ │ │ └── interface.j2 +│ │ └── srx/ # SRX firewall +│ ├── huawei/ # Huawei devices +│ │ └── vrp/ +│ ├── linux/ # Linux servers +│ │ ├── network.j2 +│ │ └── firewall.j2 +│ └── schemas/ # JSON Schema definitions +│ ├── cisco_ospf.json +│ ├── cisco_bgp.json +│ ├── cisco_interface.json +│ └── common.json +├── config_renderer.py # Template rendering engine +└── config_validator.py # Schema validation +``` + +## Template Examples + +### 1. OSPF Configuration Template (Cisco IOS) + +**File**: `config_templates/cisco/ios/ospf.j2` + +```jinja2 +{# OSPF Configuration Template for Cisco IOS #} +{% if ospf.enabled %} +router ospf {{ ospf.process_id }} +{% if ospf.router_id %} + router-id {{ ospf.router_id }} +{% endif %} +{% for network in ospf.networks %} + network {{ network.address }} mask {{ network.wildcard }} area {{ network.area }} +{% endfor %} +{% if ospf.passive_interfaces %} +{% for interface in ospf.passive_interfaces %} + passive-interface {{ interface }} +{% endfor %} +{% endif %} +{% if ospf.auto_cost_reference %} + auto-cost reference-bandwidth {{ ospf.auto_cost_reference }} +{% endif %} +{% if ospf.default_information_originate %} + default-information originate{{ ' metric' if ospf.default_metric else '' }}{{ ospf.default_metric if ospf.default_metric else '' }} +{% endif %} +! +{% endif %} + +{# OSPF Interface Configuration #} +{% for iface in ospf.interfaces %} +interface {{ iface.name }} +{% if iface.cost %} + ip ospf cost {{ iface.cost }} +{% endif %} +{% if iface.hello_interval %} + ip ospf hello-interval {{ iface.hello_interval }} +{% endif %} +{% if iface.dead_interval %} + ip ospf dead-interval {{ iface.dead_interval }} +{% endif %} +{% if iface.authentication %} + ip ospf authentication{{ ' message-digest' if iface.auth_type == 'message-digest' else '' }} +{% if iface.auth_key %} + ip ospf authentication-key {{ iface.auth_key }} +{% endif %} +{% if iface.auth_md5_keys %} +{% for key in iface.auth_md5_keys %} + ip ospf message-digest-key {{ key.id }} md5 {{ key.secret }} +{% endfor %} +{% endif %} +{% endif %} +{% if iface.area %} + ip ospf {{ ospf.process_id }} area {{ iface.area }} +{% endif %} +! +{% endfor %} +``` + +### 2. AI Output (JSON) for OSPF + +```json +{ + "ospf": { + "enabled": true, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + { + "address": "192.168.1.0", + "wildcard": "0.0.0.255", + "area": 0 + }, + { + "address": "10.0.0.0", + "wildcard": "0.255.255.255", + "area": 1 + } + ], + "passive_interfaces": ["GigabitEthernet0/0"], + "interfaces": [ + { + "name": "GigabitEthernet0/1", + "cost": 10, + "area": 0, + "hello_interval": 10, + "dead_interval": 40 + } + ], + "auto_cost_reference": 100000, + "default_information_originate": true, + "default_metric": 100 + } +} +``` + +### 3. Rendered Configuration Output + +```cisco +router ospf 100 + router-id 1.1.1.1 + network 192.168.1.0 mask 0.0.0.255 area 0 + network 10.0.0.0 mask 0.255.255.255 area 1 + passive-interface GigabitEthernet0/0 + auto-cost reference-bandwidth 100000 + default-information originate metric 100 +! +interface GigabitEthernet0/1 + ip ospf cost 10 + ip ospf hello-interval 10 + ip ospf dead-interval 40 + ip ospf 100 area 0 +! +``` + +## BGP Configuration Template + +**File**: `config_templates/cisco/ios/bgp.j2` + +```jinja2 +{% if bgp.enabled %} +router bgp {{ bgp.as_number }} +{% if bgp.router_id %} + bgp router-id {{ bgp.router_id }} +{% endif %} +{% if bgp.log_neighbor_changes %} + bgp log-neighbor-changes +{% endif %} +{% if bgp.graceful_restart %} + bgp graceful-restart +{% endif %} + +{# BGP Neighbors #} +{% for neighbor in bgp.neighbors %} + neighbor {{ neighbor.ip }} remote-as {{ neighbor.remote_as }} + {% if neighbor.description %} + neighbor {{ neighbor.ip }} description {{ neighbor.description }} + {% endif %} + {% if neighbor.ebgp_multihop %} + neighbor {{ neighbor.ip }} ebgp-multihop {{ neighbor.ebgp_multihop }} + {% endif %} + {% if neighbor.next_hop_self %} + neighbor {{ neighbor.ip }} next-hop-self + {% endif %} + {% if neighbor.remove_private_as %} + neighbor {{ neighbor.ip }} remove-private-as + {% endif %} + {% if neighbor.route_map_in %} + neighbor {{ neighbor.ip }} route-map {{ neighbor.route_map_in }} in + {% endif %} + {% if neighbor.route_map_out %} + neighbor {{ neighbor.ip }} route-map {{ neighbor.route_map_out }} out + {% endif %} + {% if neighbor.password %} + neighbor {{ neighbor.ip }} password {{ neighbor.password }} + {% endif %} +{% endfor %} + +{# Address Families #} +{% for af in bgp.address_families %} + address-family {{ af.type }} {{ af.vrf if af.vrf else '' }} + {% if af.redistribute_connected %} + redistribute connected + {% endif %} + {% if af.redistribute_static %} + redistribute static + {% endif %} + {% if af.redistribute_ospf %} + redistribute ospf {{ af.redistribute_ospf }} + {% endif %} + {% if af.networks %} + {% for network in af.networks %} + network {{ network.address }} mask {{ network.mask }} + {% endfor %} + {% endif %} + {% for neighbor in af.neighbors %} + neighbor {{ neighbor.ip }} activate + {% if neighbor.route_map_in %} + neighbor {{ neighbor.ip }} route-map {{ neighbor.route_map_in }} in + {% endif %} + {% if neighbor.route_map_out %} + neighbor {{ neighbor.ip }} route-map {{ neighbor.route_map_out }} out + {% endif %} + {% if neighbor.soft_reconfiguration_inbound %} + neighbor {{ neighbor.ip }} soft-reconfiguration inbound + {% endif %} + {% endfor %} + exit-address-family +{% endfor %} +! +{% endif %} +``` + +### AI Output Example for BGP + +**User Input**: +``` +"Configure BGP with local AS 65001, establish eBGP with 192.168.1.2 (AS 65002), +advertise network 10.1.0.0/16 to IPv4" +``` + +**AI Output**: +```json +{ + "bgp": { + "enabled": true, + "as_number": 65001, + "router_id": "1.1.1.1", + "log_neighbor_changes": true, + "neighbors": [ + { + "ip": "192.168.1.2", + "remote_as": 65002, + "description": "ISP_Peer", + "ebgp_multihop": 2 + } + ], + "address_families": [ + { + "type": "ipv4", + "networks": [ + { + "address": "10.1.0.0", + "mask": "255.255.0.0" + } + ], + "neighbors": [ + { + "ip": "192.168.1.2", + "activate": true + } + ] + } + ] + } +} +``` + +## Interface Configuration Template + +**File**: `config_templates/cisco/ios/interface.j2` + +```jinja2 +{% for iface in interfaces %} +interface {{ iface.name }} +{% if iface.description %} + description {{ iface.description }} +{% endif %} +{% if iface.ip_address %} + ip address {{ iface.ip_address }} {{ iface.subnet_mask }} +{% endif %} +{% if iface.ipv6_address %} + ipv6 address {{ iface.ipv6_address }} +{% endif %} +{% if iface.secondary_ips %} +{% for secondary in iface.secondary_ips %} + ip address {{ secondary.address }} {{ secondary.mask }} secondary +{% endfor %} +{% endif %} +{% if iface.enabled is defined %} +{% if not iface.enabled %} + shutdown +{% else %} + no shutdown +{% endif %} +{% endif %} +{% if iface.mtu %} + mtu {{ iface.mtu }} +{% endif %} +{% if iface.bandwidth %} + bandwidth {{ iface.bandwidth }} +{% endif %} +{% if iface.speed %} + speed {{ iface.speed }} +{% endif %} +{% if iface.duplex %} + duplex {{ iface.duplex }} +{% endif %} +{% if iface.acl_in %} + ip access-group {{ iface.acl_in }} in +{% endif %} +{% if iface.acl_out %} + ip access-group {{ iface.acl_out }} out +{% endif %} +{% if iface.nat_outside %} + ip nat outside +{% endif %} +{% if iface.nat_inside %} + ip nat inside +{% endif %} +{% if iface.vlan %} + switchport access vlan {{ iface.vlan }} +{% endif %} +{% if iface.trunk_vlans %} + switchport trunk encapsulation dot1q + switchport mode trunk + switchport trunk allowed vlan {{ iface.trunk_vlans }} +{% endif %} +! +{% endfor %} +``` + +## JSON Schema Definitions + +### OSPF Schema + +**File**: `config_templates/schemas/cisco_ospf.json` + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cisco OSPF Configuration", + "type": "object", + "properties": { + "ospf": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable OSPF routing" + }, + "process_id": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "OSPF process ID (1-65535)" + }, + "router_id": { + "type": "string", + "pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", + "description": "Router ID in dotted decimal notation" + }, + "networks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "description": "Network address" + }, + "wildcard": { + "type": "string", + "description": "Wildcard mask" + }, + "area": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "description": "OSPF area ID" + } + }, + "required": ["address", "wildcard", "area"] + } + }, + "passive_interfaces": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of passive interfaces" + }, + "interfaces": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Interface name (e.g., GigabitEthernet0/0)" + }, + "cost": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Interface OSPF cost" + }, + "area": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "description": "OSPF area for this interface" + }, + "hello_interval": { + "type": "integer", + "minimum": 1, + "maximum": 8192, + "description": "OSPF hello interval in seconds" + }, + "dead_interval": { + "type": "integer", + "minimum": 1, + "maximum": 32768, + "description": "OSPF dead interval in seconds" + }, + "authentication": { + "type": "boolean", + "description": "Enable OSPF authentication" + }, + "auth_type": { + "type": "string", + "enum": ["simple", "message-digest"], + "description": "Authentication type" + }, + "auth_key": { + "type": "string", + "description": "Simple authentication key" + }, + "auth_md5_keys": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 255 + }, + "secret": { + "type": "string" + } + }, + "required": ["id", "secret"] + } + } + }, + "required": ["name"] + } + }, + "auto_cost_reference": { + "type": "integer", + "description": "Reference bandwidth for auto cost (Mbps)" + }, + "default_information_originate": { + "type": "boolean", + "description": "Advertise default route" + }, + "default_metric": { + "type": "integer", + "description": "Default route metric" + } + }, + "required": ["enabled", "process_id"] + } + } +} +``` + +### BGP Schema + +**File**: `config_templates/schemas/cisco_bgp.json` + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cisco BGP Configuration", + "type": "object", + "properties": { + "bgp": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "as_number": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "router_id": { + "type": "string", + "pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$" + }, + "log_neighbor_changes": { + "type": "boolean" + }, + "graceful_restart": { + "type": "boolean" + }, + "neighbors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "remote_as": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "description": { + "type": "string" + }, + "ebgp_multihop": { + "type": "integer", + "minimum": 1, + "maximum": 255 + }, + "next_hop_self": { + "type": "boolean" + }, + "remove_private_as": { + "type": "boolean" + }, + "route_map_in": { + "type": "string" + }, + "route_map_out": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["ip", "remote_as"] + } + }, + "address_families": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["ipv4", "ipv6", "vpnv4", "vpnv6"] + }, + "vrf": { + "type": "string" + }, + "redistribute_connected": { + "type": "boolean" + }, + "redistribute_static": { + "type": "boolean" + }, + "redistribute_ospf": { + "type": "integer" + }, + "networks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "mask": { + "type": "string" + } + }, + "required": ["address", "mask"] + } + }, + "neighbors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ip": { + "type": "string" + }, + "activate": { + "type": "boolean" + }, + "route_map_in": { + "type": "string" + }, + "route_map_out": { + "type": "string" + }, + "soft_reconfiguration_inbound": { + "type": "boolean" + } + }, + "required": ["ip"] + } + } + }, + "required": ["type"] + } + } + }, + "required": ["enabled", "as_number"] + } + } +} +``` + +## Implementation + +### Config Renderer Module + +**File**: `gns3server/agent/gns3_copilot/config_renderer.py` + +```python +""" +Configuration Renderer for GNS3 AI Copilot + +This module handles rendering of network device configurations +using Jinja2 templates based on AI-generated structured data. +""" + +import os +import json +from typing import Dict, Any, Optional +from jinja2 import Environment, FileSystemLoader, TemplateError, select_autoescape +from pathlib import Path +import logging + +log = logging.getLogger(__name__) + + +class ConfigRenderer: + """ + Renders network device configurations using Jinja2 templates. + + The AI generates structured data (JSON), which is then validated + against schemas and rendered through vendor-specific templates. + """ + + def __init__(self, template_dir: Optional[str] = None): + """ + Initialize the configuration renderer. + + Args: + template_dir: Path to template directory. If None, uses default. + """ + if template_dir is None: + template_dir = Path(__file__).parent / "config_templates" + + self.template_dir = Path(template_dir) + self.env = Environment( + loader=FileSystemLoader(str(self.template_dir)), + autoescape=select_autoescape(), + trim_blocks=True, + lstrip_blocks=True + ) + + log.info(f"ConfigRenderer initialized with templates from: {self.template_dir}") + + def render( + self, + vendor: str, + os_type: str, + feature: str, + data: Dict[str, Any] + ) -> str: + """ + Render a configuration template. + + Args: + vendor: Vendor name (cisco, juniper, huawei, etc.) + os_type: OS type (ios, junos, vrp, etc.) + feature: Feature name (ospf, bgp, interface, etc.) + data: Structured data from AI + + Returns: + Rendered configuration as string + + Raises: + TemplateError: If template rendering fails + FileNotFoundError: If template doesn't exist + """ + template_path = f"{vendor}/{os_type}/{feature}.j2" + + try: + template = self.env.get_template(template_path) + config = template.render(**data) + log.info(f"Successfully rendered template: {template_path}") + return config + except TemplateError as e: + log.error(f"Template rendering error for {template_path}: {e}") + raise + except Exception as e: + log.error(f"Unexpected error rendering {template_path}: {e}") + raise + + def render_multi( + self, + vendor: str, + os_type: str, + features: Dict[str, Dict[str, Any]] + ) -> str: + """ + Render multiple configuration templates and combine them. + + Args: + vendor: Vendor name + os_type: OS type + features: Dictionary of feature names and their data + + Returns: + Combined configuration + """ + configs = [] + for feature, data in features.items(): + config = self.render(vendor, os_type, feature, data) + configs.append(config) + + return "\n".join(configs) + + def get_available_templates(self) -> Dict[str, list]: + """ + Get list of available templates organized by vendor and OS. + + Returns: + Dictionary with vendors as keys and list of available features + """ + templates = {} + template_path = Path(self.template_dir) + + for vendor_dir in template_path.iterdir(): + if vendor_dir.is_dir() and not vendor_dir.name.startswith('_'): + vendor = vendor_dir.name + templates[vendor] = {} + + for os_dir in vendor_dir.iterdir(): + if os_dir.is_dir(): + os_type = os_dir.name + templates[vendor][os_type] = [] + + for template_file in os_dir.glob("*.j2"): + feature = template_file.stem + templates[vendor][os_type].append(feature) + + return templates + + def validate_data(self, schema_name: str, data: Dict[str, Any]) -> bool: + """ + Validate structured data against JSON schema. + + Args: + schema_name: Name of schema file + data: Data to validate + + Returns: + True if valid, raises ValidationError otherwise + """ + # Import jsonschema only when needed + try: + from jsonschema import validate, ValidationError + except ImportError: + log.warning("jsonschema not installed, skipping validation") + return True + + schema_path = self.template_dir / "schemas" / f"{schema_name}.json" + + if not schema_path.exists(): + log.warning(f"Schema not found: {schema_path}") + return True + + with open(schema_path, 'r') as f: + schema = json.load(f) + + try: + validate(instance=data, schema=schema) + return True + except ValidationError as e: + log.error(f"Schema validation failed: {e.message}") + raise + + +class ConfigBuilder: + """ + Builds complete device configurations by combining multiple features. + """ + + def __init__(self, renderer: ConfigRenderer): + self.renderer = renderer + + def build_device_config( + self, + vendor: str, + os_type: str, + config_data: Dict[str, Any] + ) -> str: + """ + Build a complete device configuration. + + Args: + vendor: Vendor name + os_type: OS type + config_data: Dictionary with all feature configurations + + Returns: + Complete device configuration + """ + # Extract metadata + hostname = config_data.get("hostname", "Router") + config_parts = [f"hostname {hostname}\n"] + + # Render each feature section + feature_order = [ + "interface", + "vlan", + "ospf", + "bgp", + "eigrp", + "rip", + "acl", + "nat", + "qos", + "multicast" + ] + + for feature in feature_order: + if feature in config_data: + try: + config = self.renderer.render( + vendor, os_type, feature, + {feature: config_data[feature]} + ) + config_parts.append(config) + except Exception as e: + log.warning(f"Failed to render {feature}: {e}") + + return "\n".join(config_parts) +``` + +### Usage Example + +```python +from gns3server.agent.gns3_copilot.config_renderer import ConfigRenderer, ConfigBuilder + +# Initialize renderer +renderer = ConfigRenderer() + +# AI-generated data for OSPF configuration +ai_data = { + "ospf": { + "enabled": True, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + {"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0} + ], + "passive_interfaces": ["GigabitEthernet0/0"] + } +} + +# Render configuration +config = renderer.render( + vendor="cisco", + os_type="ios", + feature="ospf", + data=ai_data +) + +print(config) + +# Build complete device configuration +builder = ConfigBuilder(renderer) +full_config = builder.build_device_config( + vendor="cisco", + os_type="ios", + config_data={ + "hostname": "R1", + "interface": {...}, + "ospf": {...}, + "bgp": {...} + } +) +``` + +## Integration with AI Copilot + +### New Tool for Configuration Rendering + +```python +""" +File: gns3server/agent/gns3_copilot/tools_v2/gns3_render_config.py +""" + +from langchain_core.tools import tool +from typing import Dict, Any +import logging + +log = logging.getLogger(__name__) + + +@tool +def render_device_config( + node_id: str, + vendor: str, + os_type: str, + config_data: Dict[str, Any] +) -> str: + """ + Render network device configuration using Jinja2 templates. + + Instead of generating full configuration text, the AI should provide + structured data (dict) that will be rendered through templates. + + Args: + node_id: GNS3 node identifier + vendor: Device vendor (cisco, juniper, huawei, etc.) + os_type: Operating system type (ios, junos, vrp, nexus, etc.) + config_data: Structured configuration data from AI + + Returns: + Rendered configuration string + + Example: + >>> ai_output = { + ... "ospf": { + ... "enabled": True, + ... "process_id": 100, + ... "router_id": "1.1.1.1", + ... "networks": [ + ... {"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0} + ... ] + ... } + ... } + >>> config = render_device_config( + ... node_id="node-1", + ... vendor="cisco", + ... os_type="ios", + ... config_data=ai_output + ... ) + """ + from gns3server.agent.gns3_copilot.config_renderer import ConfigRenderer + + renderer = ConfigRenderer() + + # Validate against schema if available + try: + renderer.validate_data(f"{vendor}_{list(config_data.keys())[0]}", config_data) + except Exception as e: + log.warning(f"Schema validation failed: {e}") + + # Render configuration + try: + if len(config_data) == 1: + # Single feature + feature = list(config_data.keys())[0] + config = renderer.render(vendor, os_type, feature, config_data) + else: + # Multiple features + config = renderer.render_multi(vendor, os_type, config_data) + + return config + + except Exception as e: + return f"Error rendering configuration: {str(e)}" +``` + +## Benefits + +1. **Reliability**: Templates are tested and verified, reducing configuration errors +2. **Efficiency**: AI generates less text (structured data only), saving tokens and processing time +3. **Consistency**: Uniform configuration style across all generated configs +4. **Maintainability**: Templates are version controlled and easy to update +5. **Scalability**: Easy to add new vendors and features +6. **Validation**: JSON schemas ensure data correctness before rendering +7. **Vendor Support**: Easy to support multiple vendors with same AI logic +8. **Testing**: Templates can be unit tested independently + +## Future Enhancements + +1. **Template Marketplace**: Community-contributed templates +2. **Auto-discovery**: Detect device vendor/OS from GNS3 node type +3. **Config Diff**: Show differences before/after configuration +4. **Best Practices**: Templates embed industry best practices +5. **Validation**: Post-render validation against device syntax +6. **Rollback**: Auto-generate rollback configurations +7. **Documentation**: Templates include inline documentation + +## Example Prompts for AI + +The AI should be prompted to generate structured data instead of full configs: + +**Good Prompt**: +``` +"Generate OSPF configuration with process ID 100, router-id 1.1.1.1, +include network 192.168.1.0/24 in area 0. Output as structured JSON data." +``` + +**AI Output**: +```json +{ + "ospf": { + "enabled": true, + "process_id": 100, + "router_id": "1.1.1.1", + "networks": [ + {"address": "192.168.1.0", "wildcard": "0.0.0.255", "area": 0} + ] + } +} +``` + +Then the agent calls `render_device_config` tool to get the actual configuration.