Spaces:
Sleeping
Sleeping
| #!/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) | |