#!/usr/bin/env python3 """ Deploy configurations to GNS3 lab devices. This script demonstrates real-world usage of Stage 6 deployment engine with your GNS3 lab at lab.grahampaasch.com:3080. Usage: # Dry-run (validate only, don't deploy) python deploy_to_gns3_lab.py --dry-run # Deploy to single device python deploy_to_gns3_lab.py --device R1 # Deploy to all devices python deploy_to_gns3_lab.py --all # Deploy with custom credentials python deploy_to_gns3_lab.py --device SW1 --username admin --password cisco """ import sys import argparse import logging from pathlib import Path from typing import List, Dict # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) from agent.device_driver import DeviceType from agent.deployment_engine import DeploymentEngine, DeploymentTask, DeploymentStatus # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # GNS3 Lab Device Inventory # Update these with your actual lab devices GNS3_DEVICES = [ { 'name': 'R1', 'hostname': '192.168.1.1', # Update with actual management IP 'device_type': DeviceType.CISCO_IOS, 'vendor': 'cisco', 'model': '7200', 'role': 'router', 'description': 'Core Router 1' }, { 'name': 'R2', 'hostname': '192.168.1.2', 'device_type': DeviceType.CISCO_IOS, 'vendor': 'cisco', 'model': '7200', 'role': 'router', 'description': 'Core Router 2' }, { 'name': 'SW1', 'hostname': '192.168.1.10', 'device_type': DeviceType.CISCO_IOS, 'vendor': 'cisco', 'model': 'catalyst', 'role': 'switch', 'description': 'Access Switch 1' }, { 'name': 'SW2', 'hostname': '192.168.1.11', 'device_type': DeviceType.CISCO_IOS, 'vendor': 'cisco', 'model': 'catalyst', 'role': 'switch', 'description': 'Access Switch 2' }, ] # Sample configurations # In production, these would come from templates + network model DEVICE_CONFIGS = { 'R1': """ ! R1 Configuration hostname R1 ! ip domain-name lab.grahampaasch.com ! interface GigabitEthernet0/0 description Link to R2 ip address 10.0.0.1 255.255.255.252 no shutdown ! interface GigabitEthernet0/1 description Link to SW1 ip address 10.0.1.1 255.255.255.0 no shutdown ! router ospf 1 network 10.0.0.0 0.0.0.3 area 0 network 10.0.1.0 0.0.0.255 area 0 ! ntp server 192.168.1.1 ! end """, 'R2': """ ! R2 Configuration hostname R2 ! ip domain-name lab.grahampaasch.com ! interface GigabitEthernet0/0 description Link to R1 ip address 10.0.0.2 255.255.255.252 no shutdown ! interface GigabitEthernet0/1 description Link to SW2 ip address 10.0.2.1 255.255.255.0 no shutdown ! router ospf 1 network 10.0.0.0 0.0.0.3 area 0 network 10.0.2.0 0.0.0.255 area 0 ! ntp server 192.168.1.1 ! end """, 'SW1': """ ! SW1 Configuration hostname SW1 ! ip domain-name lab.grahampaasch.com ! vlan 10 name Data ! vlan 20 name Voice ! interface GigabitEthernet0/1 description Uplink to R1 switchport mode trunk no shutdown ! interface GigabitEthernet0/2 description Access Port switchport mode access switchport access vlan 10 no shutdown ! interface Vlan10 ip address 10.0.1.10 255.255.255.0 ! ip default-gateway 10.0.1.1 ! ntp server 192.168.1.1 ! end """, 'SW2': """ ! SW2 Configuration hostname SW2 ! ip domain-name lab.grahampaasch.com ! vlan 10 name Data ! vlan 20 name Voice ! interface GigabitEthernet0/1 description Uplink to R2 switchport mode trunk no shutdown ! interface GigabitEthernet0/2 description Access Port switchport mode access switchport access vlan 10 no shutdown ! interface Vlan10 ip address 10.0.2.10 255.255.255.0 ! ip default-gateway 10.0.2.1 ! ntp server 192.168.1.1 ! end """, } def deploy_to_device( device: Dict, config: str, username: str, password: str, dry_run: bool = True, engine: DeploymentEngine = None ) -> DeploymentStatus: """ Deploy configuration to a single device. Args: device: Device dictionary config: Configuration to deploy username: Device username password: Device password dry_run: If True, validate only engine: DeploymentEngine instance Returns: DeploymentStatus """ if engine is None: engine = DeploymentEngine(use_napalm=True) logger.info(f"{'[DRY-RUN] ' if dry_run else ''}Deploying to {device['name']} ({device['hostname']})...") # Define validation checks pre_checks = [ "command:show version", # Verify device is reachable ] post_checks = [ "command:show running-config | include hostname", # Verify config applied ] # Create deployment task task = DeploymentTask( device_id=device['name'], device_type=device['device_type'], hostname=device['hostname'], username=username, password=password, config=config, dry_run=dry_run, pre_checks=pre_checks, post_checks=post_checks ) # Deploy result = engine.deploy_single_device(task) # Log results if result.status == DeploymentStatus.SUCCESS: logger.info(f"✅ {device['name']}: Deployment successful") elif result.status == DeploymentStatus.ROLLED_BACK: logger.warning(f"⚠️ {device['name']}: Deployment failed, rolled back") logger.warning(f" Error: {result.error}") else: logger.error(f"❌ {device['name']}: Deployment failed") logger.error(f" Error: {result.error}") # Show pre-check results if result.pre_check_results: logger.info(f" Pre-checks: {sum(result.pre_check_results.values())}/{len(result.pre_check_results)} passed") # Show post-check results if result.post_check_results: logger.info(f" Post-checks: {sum(result.post_check_results.values())}/{len(result.post_check_results)} passed") return result.status def deploy_to_multiple( devices: List[Dict], username: str, password: str, dry_run: bool = True, parallel: bool = False ) -> Dict[str, DeploymentStatus]: """ Deploy to multiple devices. Args: devices: List of device dictionaries username: Device username password: Device password dry_run: If True, validate only parallel: If True, deploy in parallel Returns: Dictionary of device_name -> status """ engine = DeploymentEngine(use_napalm=True) results = {} logger.info(f"\n{'='*60}") logger.info(f"Deploying to {len(devices)} devices...") logger.info(f"Mode: {'DRY-RUN' if dry_run else 'PRODUCTION'}") logger.info(f"Parallel: {parallel}") logger.info(f"{'='*60}\n") for device in devices: config = DEVICE_CONFIGS.get(device['name']) if not config: logger.warning(f"No configuration found for {device['name']}, skipping...") results[device['name']] = DeploymentStatus.FAILED continue status = deploy_to_device( device=device, config=config, username=username, password=password, dry_run=dry_run, engine=engine ) results[device['name']] = status # Summary logger.info(f"\n{'='*60}") logger.info("Deployment Summary:") logger.info(f"{'='*60}") success_count = sum(1 for s in results.values() if s == DeploymentStatus.SUCCESS) failed_count = sum(1 for s in results.values() if s == DeploymentStatus.FAILED) rollback_count = sum(1 for s in results.values() if s == DeploymentStatus.ROLLED_BACK) logger.info(f"✅ Success: {success_count}/{len(results)}") logger.info(f"❌ Failed: {failed_count}/{len(results)}") logger.info(f"⚠️ Rolled back: {rollback_count}/{len(results)}") logger.info(f"{'='*60}\n") return results def main(): """Main entry point""" parser = argparse.ArgumentParser( description='Deploy configurations to GNS3 lab devices' ) parser.add_argument( '--device', help='Deploy to specific device (e.g., R1, SW1)' ) parser.add_argument( '--all', action='store_true', help='Deploy to all devices' ) parser.add_argument( '--dry-run', action='store_true', default=True, help='Validate only, do not deploy (default: True)' ) parser.add_argument( '--production', action='store_true', help='PRODUCTION MODE: Actually deploy configs (disables dry-run)' ) parser.add_argument( '--username', default='admin', help='Device username (default: admin)' ) parser.add_argument( '--password', default='cisco', help='Device password (default: cisco)' ) parser.add_argument( '--parallel', action='store_true', help='Deploy to multiple devices in parallel' ) parser.add_argument( '--list', action='store_true', help='List available devices and exit' ) args = parser.parse_args() # List devices if args.list: print("\nAvailable GNS3 Lab Devices:") print("="*60) for dev in GNS3_DEVICES: print(f" {dev['name']:10s} - {dev['hostname']:15s} ({dev['description']})") print("="*60) return 0 # Determine dry-run mode dry_run = not args.production if args.production: logger.warning("⚠️ PRODUCTION MODE ENABLED - Configs will be deployed to real devices!") response = input("Are you sure you want to continue? (yes/no): ") if response.lower() != 'yes': logger.info("Aborted.") return 1 # Deploy to specific device if args.device: device = next((d for d in GNS3_DEVICES if d['name'] == args.device), None) if not device: logger.error(f"Device '{args.device}' not found in inventory") logger.info("Use --list to see available devices") return 1 config = DEVICE_CONFIGS.get(args.device) if not config: logger.error(f"No configuration defined for {args.device}") return 1 status = deploy_to_device( device=device, config=config, username=args.username, password=args.password, dry_run=dry_run ) return 0 if status == DeploymentStatus.SUCCESS else 1 # Deploy to all devices elif args.all: results = deploy_to_multiple( devices=GNS3_DEVICES, username=args.username, password=args.password, dry_run=dry_run, parallel=args.parallel ) failed = sum(1 for s in results.values() if s != DeploymentStatus.SUCCESS) return 0 if failed == 0 else 1 else: parser.print_help() return 1 if __name__ == '__main__': try: sys.exit(main()) except KeyboardInterrupt: logger.info("\nAborted by user") sys.exit(1) except Exception as e: logger.exception(f"Unexpected error: {e}") sys.exit(1)