+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 472 of 541

๐Ÿ“˜ Network Automation: Netmiko and NAPALM

Master network automation: netmiko and napalm in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿ’ŽAdvanced
25 min read

Prerequisites

  • Basic understanding of programming concepts ๐Ÿ“
  • Python installation (3.8+) ๐Ÿ
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write clean, Pythonic code โœจ

๐ŸŽฏ Introduction

Welcome to the exciting world of network automation! ๐ŸŽ‰ In this guide, weโ€™ll explore how Netmiko and NAPALM can revolutionize the way you manage network devices using Python.

Youโ€™ll discover how these powerful libraries can transform tedious manual network configurations into automated, reliable, and scalable solutions. Whether youโ€™re managing routers ๐Ÿ”Œ, switches ๐Ÿ–ฅ๏ธ, or firewalls ๐Ÿ›ก๏ธ, understanding network automation is essential for modern network engineering.

By the end of this tutorial, youโ€™ll feel confident automating network tasks with Python! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Network Automation

๐Ÿค” What are Netmiko and NAPALM?

Network automation is like having a smart assistant for your network devices ๐Ÿค–. Think of it as teaching your computer to speak the language of routers and switches!

Netmiko is like a universal translator ๐ŸŒ that helps Python communicate with network devices through SSH. Itโ€™s built on top of Paramiko and simplifies connecting to various vendor devices.

NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) is like a Swiss Army knife ๐Ÿ”ง for network automation. It provides a unified API to interact with different network device operating systems.

In Python terms, these libraries help you:

  • โœจ Connect to network devices programmatically
  • ๐Ÿš€ Execute commands and retrieve outputs
  • ๐Ÿ›ก๏ธ Configure devices consistently across vendors
  • ๐Ÿ“Š Extract operational data in structured formats

๐Ÿ’ก Why Use Network Automation?

Hereโ€™s why network engineers love automation:

  1. Consistency ๐Ÿ”’: Apply configurations uniformly across devices
  2. Speed โšก: Configure hundreds of devices in minutes
  3. Accuracy ๐ŸŽฏ: Eliminate human errors from manual typing
  4. Documentation ๐Ÿ“–: Code serves as living documentation
  5. Scalability ๐Ÿ“ˆ: Manage growing networks efficiently

Real-world example: Imagine updating VLAN configurations across 100 switches ๐Ÿข. With automation, you can complete this task in minutes instead of hours!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Netmiko Basics

Letโ€™s start with connecting to a device using Netmiko:

from netmiko import ConnectHandler

# ๐Ÿ”Œ Device connection details
cisco_device = {
    'device_type': 'cisco_ios',  # ๐Ÿท๏ธ Specify device type
    'host': '192.168.1.1',        # ๐Ÿ  Device IP address
    'username': 'admin',          # ๐Ÿ‘ค Login username
    'password': 'secure_pass',    # ๐Ÿ” Login password
    'secret': 'enable_pass'       # ๐Ÿ”‘ Enable password
}

# ๐Ÿš€ Connect to the device
connection = ConnectHandler(**cisco_device)
connection.enable()  # ๐Ÿ“ˆ Enter enable mode

# ๐Ÿ’ก Send a command
output = connection.send_command('show version')
print(f"๐Ÿ–ฅ๏ธ Device info:\n{output}")

# ๐Ÿ”’ Always close the connection!
connection.disconnect()

๐Ÿ’ก Explanation: Notice how we specify the device type! Netmiko supports many vendors including Cisco, Juniper, Arista, and more.

๐ŸŽฏ NAPALM Basics

Hereโ€™s how to use NAPALM for vendor-agnostic operations:

from napalm import get_network_driver

# ๐ŸŽจ Get the appropriate driver
driver = get_network_driver('ios')  # ๐Ÿท๏ธ Cisco IOS driver

# ๐Ÿ”ง Create device object
device = driver(
    hostname='192.168.1.1',
    username='admin',
    password='secure_pass',
    optional_args={'secret': 'enable_pass'}
)

# ๐ŸŒŸ Open connection
device.open()

# ๐Ÿ“Š Get device facts
facts = device.get_facts()
print(f"๐Ÿข Device Model: {facts['model']}")
print(f"๐Ÿ”ข Serial Number: {facts['serial_number']}")
print(f"๐Ÿ’พ OS Version: {facts['os_version']}")

# ๐ŸŽฏ Get interfaces
interfaces = device.get_interfaces()
for intf, details in interfaces.items():
    print(f"๐Ÿ”Œ {intf}: {'UP' if details['is_up'] else 'DOWN'} ๐ŸŸข")

# ๐Ÿ”’ Close connection
device.close()

๐Ÿ’ก Practical Examples

๐Ÿข Example 1: Bulk Configuration Updater

Letโ€™s build a tool to update configurations across multiple devices:

from netmiko import ConnectHandler
from concurrent.futures import ThreadPoolExecutor
import logging

# ๐Ÿ“ Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class NetworkConfigurator:
    def __init__(self, devices):
        self.devices = devices  # ๐Ÿ“‹ List of device dictionaries
        
    def configure_device(self, device):
        """๐Ÿ”ง Configure a single device"""
        try:
            # ๐Ÿš€ Connect to device
            logger.info(f"๐Ÿ”Œ Connecting to {device['host']}")
            connection = ConnectHandler(**device)
            connection.enable()
            
            # ๐Ÿ“‹ Configuration commands
            config_commands = [
                'ntp server 10.1.1.1',           # โฐ NTP server
                'logging host 10.1.1.100',       # ๐Ÿ“Š Syslog server
                'banner motd # Authorized Use Only! ๐Ÿ”’ #',  # ๐Ÿšช Login banner
                'service timestamps debug datetime msec',     # โฑ๏ธ Timestamps
            ]
            
            # ๐Ÿ’ซ Send configuration
            output = connection.send_config_set(config_commands)
            logger.info(f"โœ… Configured {device['host']} successfully!")
            
            # ๐Ÿ’พ Save configuration
            connection.save_config()
            logger.info(f"๐Ÿ’พ Configuration saved on {device['host']}")
            
            connection.disconnect()
            return {'device': device['host'], 'status': 'success', 'output': output}
            
        except Exception as e:
            logger.error(f"โŒ Failed to configure {device['host']}: {str(e)}")
            return {'device': device['host'], 'status': 'failed', 'error': str(e)}
    
    def configure_all(self, max_threads=5):
        """๐Ÿš€ Configure all devices in parallel"""
        results = []
        
        # ๐Ÿƒโ€โ™‚๏ธ Use ThreadPoolExecutor for parallel execution
        with ThreadPoolExecutor(max_workers=max_threads) as executor:
            futures = [executor.submit(self.configure_device, device) 
                      for device in self.devices]
            
            # ๐Ÿ“Š Collect results
            for future in futures:
                results.append(future.result())
        
        # ๐Ÿ“ˆ Summary
        success_count = sum(1 for r in results if r['status'] == 'success')
        print(f"\n๐ŸŽ‰ Configuration Summary:")
        print(f"โœ… Successful: {success_count}/{len(self.devices)}")
        print(f"โŒ Failed: {len(self.devices) - success_count}/{len(self.devices)}")
        
        return results

# ๐ŸŽฎ Let's use it!
devices = [
    {
        'device_type': 'cisco_ios',
        'host': '192.168.1.1',
        'username': 'admin',
        'password': 'pass123',
        'secret': 'enable123'
    },
    {
        'device_type': 'cisco_ios',
        'host': '192.168.1.2',
        'username': 'admin',
        'password': 'pass123',
        'secret': 'enable123'
    }
]

configurator = NetworkConfigurator(devices)
results = configurator.configure_all()

๐ŸŽฏ Try it yourself: Add error handling for specific configuration failures and implement rollback functionality!

๐ŸŽฎ Example 2: Network Health Monitor

Letโ€™s create a comprehensive network health monitoring system:

from napalm import get_network_driver
import json
from datetime import datetime
import pandas as pd

class NetworkHealthMonitor:
    def __init__(self):
        self.health_data = []  # ๐Ÿ“Š Store health metrics
        
    def check_device_health(self, device_info):
        """๐Ÿฅ Check health of a single device"""
        driver = get_network_driver(device_info['driver'])
        device = driver(**device_info['connection_params'])
        
        try:
            device.open()
            health_report = {
                'timestamp': datetime.now().isoformat(),  # โฐ Current time
                'hostname': device_info['connection_params']['hostname'],
                'checks': {}
            }
            
            # ๐Ÿ” Check 1: Device facts
            facts = device.get_facts()
            health_report['device_info'] = {
                'model': facts['model'],
                'uptime': facts['uptime'],
                'vendor': facts['vendor']
            }
            
            # ๐Ÿ’พ Check 2: Memory usage
            environment = device.get_environment()
            if 'memory' in environment:
                memory = environment['memory']
                used_percent = (memory['used_ram'] / memory['available_ram']) * 100
                health_report['checks']['memory'] = {
                    'status': 'โœ…' if used_percent < 80 else 'โš ๏ธ',
                    'used_percent': round(used_percent, 2),
                    'message': f"Memory usage: {used_percent:.1f}%"
                }
            
            # ๐ŸŒก๏ธ Check 3: CPU temperature
            if 'cpu' in environment:
                cpu_temps = [cpu['temperature'] for cpu in environment['cpu'].values()]
                avg_temp = sum(cpu_temps) / len(cpu_temps)
                health_report['checks']['temperature'] = {
                    'status': 'โœ…' if avg_temp < 70 else '๐Ÿ”ฅ',
                    'avg_celsius': round(avg_temp, 1),
                    'message': f"CPU temp: {avg_temp:.1f}ยฐC"
                }
            
            # ๐Ÿ”Œ Check 4: Interface errors
            interfaces = device.get_interfaces_counters()
            error_interfaces = []
            for intf, counters in interfaces.items():
                if counters['rx_errors'] > 100 or counters['tx_errors'] > 100:
                    error_interfaces.append(intf)
            
            health_report['checks']['interfaces'] = {
                'status': 'โœ…' if not error_interfaces else 'โš ๏ธ',
                'error_count': len(error_interfaces),
                'message': f"Interfaces with errors: {len(error_interfaces)}"
            }
            
            # ๐ŸŽฏ Overall health score
            health_score = self._calculate_health_score(health_report['checks'])
            health_report['health_score'] = health_score
            health_report['health_emoji'] = self._get_health_emoji(health_score)
            
            device.close()
            return health_report
            
        except Exception as e:
            logger.error(f"โŒ Health check failed: {str(e)}")
            return {
                'hostname': device_info['connection_params']['hostname'],
                'status': 'failed',
                'error': str(e)
            }
    
    def _calculate_health_score(self, checks):
        """๐Ÿ“Š Calculate overall health score (0-100)"""
        scores = {
            'โœ…': 100,
            'โš ๏ธ': 70,
            'โŒ': 30,
            '๐Ÿ”ฅ': 50
        }
        
        if not checks:
            return 0
            
        total_score = sum(scores.get(check['status'], 0) for check in checks.values())
        return total_score // len(checks)
    
    def _get_health_emoji(self, score):
        """๐ŸŽจ Get emoji based on health score"""
        if score >= 90:
            return "๐Ÿ’š"  # Excellent
        elif score >= 70:
            return "๐Ÿ’›"  # Good
        elif score >= 50:
            return "๐Ÿงก"  # Warning
        else:
            return "โค๏ธ"  # Critical
    
    def generate_report(self, devices):
        """๐Ÿ“ˆ Generate health report for all devices"""
        print("๐Ÿฅ Network Health Check Report")
        print("=" * 50)
        
        for device in devices:
            report = self.check_device_health(device)
            if 'error' not in report:
                print(f"\n๐Ÿข Device: {report['hostname']}")
                print(f"   Health: {report['health_emoji']} {report['health_score']}%")
                for check_name, check_data in report['checks'].items():
                    print(f"   {check_data['status']} {check_name}: {check_data['message']}")
            else:
                print(f"\nโŒ Device: {report['hostname']} - Check Failed!")

# ๐ŸŽฎ Usage example
devices = [
    {
        'driver': 'ios',
        'connection_params': {
            'hostname': '192.168.1.1',
            'username': 'admin',
            'password': 'pass123',
            'optional_args': {'secret': 'enable123'}
        }
    }
]

monitor = NetworkHealthMonitor()
monitor.generate_report(devices)

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Configuration Templating with Jinja2

When youโ€™re ready to level up, combine network automation with templating:

from jinja2 import Template
from netmiko import ConnectHandler

# ๐ŸŽจ Create a configuration template
vlan_template = Template("""
{% for vlan in vlans %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
 {% if vlan.description %}
 description {{ vlan.description }} ๐Ÿท๏ธ
 {% endif %}
!
interface vlan {{ vlan.id }}
 description {{ vlan.name }} SVI ๐ŸŒ
 ip address {{ vlan.ip }} {{ vlan.mask }}
 no shutdown
!
{% endfor %}
""")

# ๐Ÿ“Š VLAN data
vlan_data = {
    'vlans': [
        {'id': 10, 'name': 'SALES', 'description': 'Sales Department', 
         'ip': '10.1.10.1', 'mask': '255.255.255.0'},
        {'id': 20, 'name': 'IT', 'description': 'IT Department',
         'ip': '10.1.20.1', 'mask': '255.255.255.0'},
        {'id': 30, 'name': 'GUEST', 'description': 'Guest Network',
         'ip': '10.1.30.1', 'mask': '255.255.255.0'}
    ]
}

# ๐Ÿ”ง Generate configuration
config = vlan_template.render(vlan_data)
print("๐Ÿ“‹ Generated Configuration:")
print(config)

# ๐Ÿš€ Apply to device
def apply_templated_config(device_params, config_text):
    connection = ConnectHandler(**device_params)
    connection.enable()
    
    # ๐Ÿ’ซ Send configuration
    output = connection.send_config_set(config_text.split('\n'))
    connection.save_config()
    connection.disconnect()
    
    return output

๐Ÿ—๏ธ Event-Driven Automation

For the brave automators, implement reactive network automation:

import asyncio
from napalm import get_network_driver
import time

class NetworkEventHandler:
    def __init__(self):
        self.thresholds = {
            'cpu_usage': 80,      # ๐Ÿ”ฅ CPU threshold
            'memory_usage': 85,   # ๐Ÿ’พ Memory threshold
            'interface_errors': 100  # โš ๏ธ Error threshold
        }
        
    async def monitor_device(self, device_info):
        """๐Ÿ” Continuously monitor device and trigger actions"""
        driver = get_network_driver(device_info['driver'])
        
        while True:
            try:
                device = driver(**device_info['connection_params'])
                device.open()
                
                # ๐Ÿ“Š Get environment data
                env = device.get_environment()
                
                # ๐Ÿ”ฅ Check CPU
                if 'cpu' in env:
                    cpu_usage = list(env['cpu'].values())[0]['%usage']
                    if cpu_usage > self.thresholds['cpu_usage']:
                        await self.handle_high_cpu(device_info, cpu_usage)
                
                # ๐Ÿ’พ Check Memory
                if 'memory' in env:
                    memory = env['memory']
                    memory_usage = (memory['used_ram'] / memory['available_ram']) * 100
                    if memory_usage > self.thresholds['memory_usage']:
                        await self.handle_high_memory(device_info, memory_usage)
                
                device.close()
                
            except Exception as e:
                print(f"โŒ Monitoring error: {str(e)}")
            
            # โฑ๏ธ Wait before next check
            await asyncio.sleep(60)  # Check every minute
    
    async def handle_high_cpu(self, device_info, cpu_usage):
        """๐Ÿ”ฅ Handle high CPU usage event"""
        print(f"๐Ÿšจ HIGH CPU ALERT on {device_info['connection_params']['hostname']}!")
        print(f"   CPU Usage: {cpu_usage}%")
        print(f"   ๐Ÿ”ง Triggering automated response...")
        
        # Implement automated response (e.g., clear ARP cache, restart process)
        # This is where you'd add your remediation logic
        
    async def handle_high_memory(self, device_info, memory_usage):
        """๐Ÿ’พ Handle high memory usage event"""
        print(f"๐Ÿšจ HIGH MEMORY ALERT on {device_info['connection_params']['hostname']}!")
        print(f"   Memory Usage: {memory_usage:.1f}%")
        print(f"   ๐Ÿ”ง Triggering automated response...")

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Not Handling Connection Timeouts

# โŒ Wrong way - no timeout handling
connection = ConnectHandler(**device_params)
output = connection.send_command('show running-config')  # ๐Ÿ’ฅ Might hang forever!

# โœ… Correct way - set timeouts
connection = ConnectHandler(
    **device_params,
    timeout=30,           # ๐Ÿ• Connection timeout
    session_timeout=60    # โฑ๏ธ Session timeout
)

# Also use command-specific timeouts
output = connection.send_command(
    'show running-config',
    read_timeout=120  # ๐Ÿ“Š Long commands need more time!
)

๐Ÿคฏ Pitfall 2: Not Saving Configurations

# โŒ Dangerous - changes lost on reboot!
connection.send_config_set(['interface gi0/1', 'description Important Link'])
connection.disconnect()  # ๐Ÿ’ฅ Config not saved!

# โœ… Safe - always save your work!
connection.send_config_set(['interface gi0/1', 'description Important Link'])
connection.save_config()  # ๐Ÿ’พ Save to startup-config
print("โœ… Configuration saved successfully!")
connection.disconnect()

๐Ÿ”’ Pitfall 3: Hardcoding Credentials

# โŒ Security nightmare - never do this!
device = {
    'username': 'admin',
    'password': 'MyPassword123!'  # ๐Ÿšจ Exposed credential!
}

# โœ… Secure way - use environment variables or vault
import os
from getpass import getpass

device = {
    'username': os.environ.get('NETWORK_USER'),
    'password': os.environ.get('NETWORK_PASS') or getpass('Password: ')
}

# ๐Ÿ” Even better - use a secrets management system!

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Use Context Managers: Always ensure connections are closed
  2. ๐Ÿ“ Log Everything: Track all changes and operations
  3. ๐Ÿ›ก๏ธ Implement Rollback: Have a way to undo changes
  4. ๐Ÿ”„ Test in Lab First: Never test automation in production
  5. โœจ Use Version Control: Track your automation scripts
  6. ๐Ÿš€ Parallelize Carefully: Donโ€™t overwhelm devices with connections
  7. ๐Ÿ“Š Monitor Impact: Track CPU/memory during automation

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Network Compliance Checker

Create a tool that checks network devices for compliance:

๐Ÿ“‹ Requirements:

  • โœ… Check for required NTP servers
  • ๐Ÿ”’ Verify SSH is enabled and Telnet is disabled
  • ๐Ÿ“Š Ensure logging is configured
  • ๐Ÿ›ก๏ธ Check for banner messages
  • ๐Ÿ“ˆ Generate compliance report

๐Ÿš€ Bonus Points:

  • Add automatic remediation for non-compliant items
  • Create a web dashboard for compliance status
  • Implement configuration backup before changes

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
from netmiko import ConnectHandler
import re
from datetime import datetime
import json

class ComplianceChecker:
    def __init__(self):
        self.compliance_rules = {
            'ntp_servers': ['10.1.1.1', '10.1.1.2'],  # ๐Ÿ• Required NTP
            'required_banner': 'Authorized',           # ๐Ÿšช Banner keyword
            'syslog_server': '10.1.1.100',            # ๐Ÿ“Š Logging host
            'ssh_enabled': True,                       # ๐Ÿ” SSH required
            'telnet_disabled': True                    # ๐Ÿšซ No telnet
        }
        
    def check_device_compliance(self, device_params):
        """๐Ÿ” Check device against compliance rules"""
        results = {
            'device': device_params['host'],
            'timestamp': datetime.now().isoformat(),
            'compliant': True,
            'checks': {}
        }
        
        try:
            connection = ConnectHandler(**device_params)
            connection.enable()
            
            # ๐Ÿ• Check NTP servers
            ntp_output = connection.send_command('show run | include ntp server')
            configured_ntp = re.findall(r'ntp server (\S+)', ntp_output)
            ntp_compliant = all(ntp in configured_ntp for ntp in self.compliance_rules['ntp_servers'])
            
            results['checks']['ntp'] = {
                'compliant': ntp_compliant,
                'status': 'โœ…' if ntp_compliant else 'โŒ',
                'message': f"NTP servers: {', '.join(configured_ntp)}"
            }
            
            # ๐Ÿšช Check banner
            banner_output = connection.send_command('show run | include banner')
            banner_compliant = self.compliance_rules['required_banner'] in banner_output
            
            results['checks']['banner'] = {
                'compliant': banner_compliant,
                'status': 'โœ…' if banner_compliant else 'โŒ',
                'message': 'Login banner configured' if banner_compliant else 'Banner missing!'
            }
            
            # ๐Ÿ“Š Check syslog
            syslog_output = connection.send_command('show run | include logging host')
            syslog_compliant = self.compliance_rules['syslog_server'] in syslog_output
            
            results['checks']['syslog'] = {
                'compliant': syslog_compliant,
                'status': 'โœ…' if syslog_compliant else 'โŒ',
                'message': f"Syslog server: {'configured' if syslog_compliant else 'not configured'}"
            }
            
            # ๐Ÿ” Check SSH/Telnet
            ssh_output = connection.send_command('show ip ssh')
            ssh_enabled = 'SSH Enabled' in ssh_output
            
            vty_output = connection.send_command('show run | section line vty')
            telnet_disabled = 'transport input ssh' in vty_output or 'transport input none' in vty_output
            
            results['checks']['remote_access'] = {
                'compliant': ssh_enabled and telnet_disabled,
                'status': 'โœ…' if (ssh_enabled and telnet_disabled) else 'โŒ',
                'message': f"SSH: {'enabled' if ssh_enabled else 'disabled'}, Telnet: {'disabled' if telnet_disabled else 'enabled'}"
            }
            
            # ๐ŸŽฏ Overall compliance
            results['compliant'] = all(check['compliant'] for check in results['checks'].values())
            results['compliance_score'] = sum(1 for check in results['checks'].values() if check['compliant']) / len(results['checks']) * 100
            
            # ๐Ÿ”ง Auto-remediation if requested
            if not results['compliant']:
                results['remediation_available'] = True
                results['remediation_commands'] = self._generate_remediation(results['checks'])
            
            connection.disconnect()
            
        except Exception as e:
            results['error'] = str(e)
            results['compliant'] = False
            
        return results
    
    def _generate_remediation(self, checks):
        """๐Ÿ”ง Generate commands to fix non-compliant items"""
        commands = []
        
        if not checks.get('ntp', {}).get('compliant'):
            for ntp in self.compliance_rules['ntp_servers']:
                commands.append(f'ntp server {ntp}')
        
        if not checks.get('banner', {}).get('compliant'):
            commands.append(f'banner motd # {self.compliance_rules["required_banner"]} Use Only! #')
        
        if not checks.get('syslog', {}).get('compliant'):
            commands.append(f'logging host {self.compliance_rules["syslog_server"]}')
        
        if not checks.get('remote_access', {}).get('compliant'):
            commands.extend([
                'ip ssh version 2',
                'line vty 0 15',
                'transport input ssh'
            ])
        
        return commands
    
    def generate_report(self, results):
        """๐Ÿ“Š Generate compliance report"""
        print("\n๐Ÿ“‹ NETWORK COMPLIANCE REPORT")
        print("=" * 60)
        print(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print(f"Devices Checked: {len(results)}")
        
        compliant_count = sum(1 for r in results if r['compliant'])
        print(f"\n๐Ÿ“Š Summary:")
        print(f"   โœ… Compliant: {compliant_count}/{len(results)}")
        print(f"   โŒ Non-Compliant: {len(results) - compliant_count}/{len(results)}")
        
        print("\n๐Ÿ” Detailed Results:")
        for result in results:
            print(f"\n๐Ÿข Device: {result['device']}")
            if 'error' in result:
                print(f"   โŒ Error: {result['error']}")
            else:
                score_emoji = '๐Ÿ’š' if result['compliance_score'] == 100 else '๐Ÿ’›' if result['compliance_score'] >= 75 else 'โค๏ธ'
                print(f"   Score: {score_emoji} {result['compliance_score']:.0f}%")
                for check_name, check_data in result['checks'].items():
                    print(f"   {check_data['status']} {check_name}: {check_data['message']}")

# ๐ŸŽฎ Test it out!
devices = [
    {
        'device_type': 'cisco_ios',
        'host': '192.168.1.1',
        'username': 'admin',
        'password': 'pass123',
        'secret': 'enable123'
    }
]

checker = ComplianceChecker()
results = [checker.check_device_compliance(device) for device in devices]
checker.generate_report(results)

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Connect to network devices programmatically with Netmiko ๐Ÿ’ช
  • โœ… Use NAPALM for vendor-agnostic operations ๐Ÿ›ก๏ธ
  • โœ… Automate configurations across multiple devices ๐ŸŽฏ
  • โœ… Build monitoring tools for network health ๐Ÿ›
  • โœ… Implement compliance checking and remediation ๐Ÿš€

Remember: Network automation is about making your life easier while improving reliability! Start small, test thoroughly, and gradually expand your automation toolkit. ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered network automation basics!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the exercises above on lab devices
  2. ๐Ÿ—๏ธ Build an automation project for your network
  3. ๐Ÿ“š Explore advanced topics like NETCONF/RESTCONF
  4. ๐ŸŒŸ Share your automation scripts with the community!

Remember: Every network automation expert started with their first script. Keep automating, keep learning, and most importantly, have fun transforming your network operations! ๐Ÿš€


Happy automating! ๐ŸŽ‰๐Ÿš€โœจ