overgrowth / examples /deploy_to_gns3_lab.py
Graham Paasch
feat: GNS3 lab deployment script with examples
6d91bcf
Raw
History Blame
11.6 kB
#!/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)