Network Automation with Ansible and NAPALM: Enterprise Infrastructure Guide
Network automation with Ansible and NAPALM revolutionizes enterprise network operations by enabling consistent, reliable, and scalable configuration management. This comprehensive guide explores advanced automation techniques, infrastructure-as-code practices, and production-ready frameworks for enterprise network environments.
Enterprise Network Automation
Section 1: Advanced Ansible Network Automation
Ansible provides powerful network automation capabilities through specialized modules and playbooks designed for network device management.
Comprehensive Ansible Network Framework
# ansible.cfg - Optimized for network automation
[defaults]
inventory = inventory/hosts.yml
host_key_checking = False
timeout = 30
gather_facts = False
retry_files_enabled = False
stdout_callback = yaml
bin_ansible_callbacks = True
[persistent_connection]
connect_timeout = 60
command_timeout = 30
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
---
# inventory/hosts.yml - Dynamic inventory structure
all:
children:
core_switches:
hosts:
core-sw-01:
ansible_host: 10.1.1.10
ansible_network_os: ios
device_type: cisco_ios
site: datacenter_a
role: core
core-sw-02:
ansible_host: 10.1.1.11
ansible_network_os: ios
device_type: cisco_ios
site: datacenter_a
role: core
vars:
ansible_connection: network_cli
ansible_user: "{{ vault_network_username }}"
ansible_password: "{{ vault_network_password }}"
ansible_become: yes
ansible_become_method: enable
ansible_become_password: "{{ vault_enable_password }}"
access_switches:
hosts:
access-sw-[01:20]:
ansible_host: 10.2.1.[10:29]
ansible_network_os: ios
device_type: cisco_ios
site: office_floor_[1:4]
role: access
vars:
ansible_connection: network_cli
ansible_user: "{{ vault_network_username }}"
ansible_password: "{{ vault_network_password }}"
routers:
children:
edge_routers:
hosts:
edge-rtr-01:
ansible_host: 192.168.1.1
ansible_network_os: iosxr
device_type: cisco_iosxr
site: edge_site_a
role: edge
wan_routers:
hosts:
wan-rtr-[01:05]:
ansible_host: 172.16.1.[1:5]
ansible_network_os: junos
device_type: juniper_junos
site: wan_site_[a:e]
role: wan
vars:
ntp_servers:
- 10.1.1.100
- 10.1.1.101
dns_servers:
- 10.1.1.200
- 10.1.1.201
syslog_server: 10.1.1.300
snmp_community: "{{ vault_snmp_community }}"
---
# playbooks/site.yml - Main orchestration playbook
- name: Enterprise Network Configuration Management
hosts: all
gather_facts: False
strategy: free
serial: "{{ serial_execution | default('20%') }}"
pre_tasks:
- name: Verify device connectivity
wait_for_connection:
timeout: 60
tags: [always, connectivity]
- name: Gather device facts
ios_facts:
gather_subset: all
when: ansible_network_os == 'ios'
tags: [facts]
- name: Create backup directory
file:
path: "{{ backup_dir }}/{{ inventory_hostname }}"
state: directory
delegate_to: localhost
run_once: True
tags: [backup]
tasks:
- name: Include device-specific tasks
include_tasks: "tasks/{{ ansible_network_os }}.yml"
tags: [configuration]
- name: Backup running configuration
include_tasks: tasks/backup.yml
tags: [backup]
- name: Validate configuration
include_tasks: tasks/validate.yml
tags: [validation]
post_tasks:
- name: Generate compliance report
include_tasks: tasks/compliance_report.yml
tags: [compliance]
- name: Send notification
include_tasks: tasks/notification.yml
when: send_notifications | default(true)
tags: [notification]
---
# playbooks/tasks/ios.yml - Cisco IOS specific tasks
- name: Configure global settings
ios_config:
lines:
- "hostname {{ inventory_hostname }}"
- "ip domain-name {{ domain_name }}"
- "service timestamps debug datetime msec"
- "service timestamps log datetime msec"
- "service password-encryption"
- "no ip http server"
- "no ip http secure-server"
backup: yes
backup_options:
filename: "{{ inventory_hostname }}_running_config_{{ ansible_date_time.iso8601_basic_short }}.txt"
dir_path: "{{ backup_dir }}/{{ inventory_hostname }}"
tags: [global_config]
- name: Configure NTP
ios_ntp:
server: "{{ item }}"
state: present
loop: "{{ ntp_servers }}"
tags: [ntp]
- name: Configure DNS
ios_config:
lines:
- "ip name-server {{ dns_servers | join(' ') }}"
tags: [dns]
- name: Configure SNMP
ios_config:
lines:
- "snmp-server community {{ snmp_community }} RO"
- "snmp-server location {{ site }}"
- "snmp-server contact {{ snmp_contact | default('Network Operations') }}"
tags: [snmp]
- name: Configure syslog
ios_config:
lines:
- "logging host {{ syslog_server }}"
- "logging trap informational"
- "logging source-interface {{ management_interface | default('GigabitEthernet0/0') }}"
tags: [syslog]
- name: Configure VLANs
ios_vlans:
config: "{{ vlans }}"
state: merged
when: vlans is defined and role == 'access'
tags: [vlans]
- name: Configure interfaces
ios_interfaces:
config: "{{ interfaces }}"
state: merged
when: interfaces is defined
tags: [interfaces]
- name: Configure L3 interfaces
ios_l3_interfaces:
config: "{{ l3_interfaces }}"
state: merged
when: l3_interfaces is defined and role in ['core', 'edge']
tags: [l3_interfaces]
---
# group_vars/all.yml - Global variables
backup_dir: "./backups"
domain_name: "enterprise.local"
management_interface: "GigabitEthernet0/0"
snmp_contact: "netops@enterprise.com"
# Security settings
security_config:
enable_ssh: true
ssh_version: 2
ssh_timeout: 300
console_timeout: 5
enable_password_encryption: true
banner_motd: |
********************************************************************************
* WARNING: This system is for authorized use only. All activities are logged. *
********************************************************************************
# QoS configuration
qos_policies:
voice:
class_map: "voice-traffic"
policy_map: "voice-policy"
bandwidth_percent: 30
video:
class_map: "video-traffic"
policy_map: "video-policy"
bandwidth_percent: 25
data:
class_map: "data-traffic"
policy_map: "data-policy"
bandwidth_percent: 45
---
# group_vars/core_switches.yml - Core switch specific variables
vlans:
- vlan_id: 10
name: "USERS"
- vlan_id: 20
name: "SERVERS"
- vlan_id: 30
name: "MGMT"
- vlan_id: 99
name: "NATIVE"
l3_interfaces:
- name: "Vlan10"
ipv4:
- address: "10.10.0.1/24"
- name: "Vlan20"
ipv4:
- address: "10.20.0.1/24"
- name: "Vlan30"
ipv4:
- address: "10.30.0.1/24"
spanning_tree:
mode: "rapid-pvst"
portfast_default: true
bpduguard_default: true
hsrp_groups:
- group: 10
ip: "10.10.0.1"
priority: 110
preempt: true
- group: 20
ip: "10.20.0.1"
priority: 110
preempt: true
---
# playbooks/deploy_change.yml - Change management playbook
- name: Network Change Deployment with Rollback Capability
hosts: "{{ target_hosts | default('all') }}"
gather_facts: False
serial: "{{ batch_size | default(1) }}"
vars:
change_id: "{{ ansible_date_time.epoch }}"
rollback_enabled: "{{ enable_rollback | default(true) }}"
validation_timeout: "{{ validation_wait | default(300) }}"
pre_tasks:
- name: Validate change window
fail:
msg: "Change deployment outside of approved maintenance window"
when:
- maintenance_window is defined
- not (ansible_date_time.hour | int >= maintenance_window.start and
ansible_date_time.hour | int <= maintenance_window.end)
tags: [validation]
- name: Create change tracking
copy:
content: |
Change ID: {{ change_id }}
Target: {{ inventory_hostname }}
Start Time: {{ ansible_date_time.iso8601 }}
Operator: {{ ansible_user_id | default('automated') }}
Change Type: {{ change_type | default('configuration') }}
dest: "{{ backup_dir }}/changes/{{ change_id }}_{{ inventory_hostname }}.log"
delegate_to: localhost
tags: [tracking]
tasks:
- name: Pre-change validation
include_tasks: tasks/pre_change_validation.yml
tags: [pre_validation]
- name: Create configuration checkpoint
ios_config:
backup: yes
backup_options:
filename: "{{ inventory_hostname }}_pre_change_{{ change_id }}.cfg"
dir_path: "{{ backup_dir }}/{{ inventory_hostname }}"
tags: [checkpoint]
- name: Apply configuration changes
include_tasks: "{{ change_playbook }}"
when: change_playbook is defined
register: change_result
tags: [apply_changes]
- name: Post-change validation
include_tasks: tasks/post_change_validation.yml
tags: [post_validation]
- name: Update change tracking
lineinfile:
path: "{{ backup_dir }}/changes/{{ change_id }}_{{ inventory_hostname }}.log"
line: |
End Time: {{ ansible_date_time.iso8601 }}
Status: {{ 'SUCCESS' if change_result.failed is not defined else 'FAILED' }}
create: yes
delegate_to: localhost
tags: [tracking]
rescue:
- name: Rollback configuration on failure
include_tasks: tasks/rollback.yml
when: rollback_enabled
tags: [rollback]
- name: Escalate failure
include_tasks: tasks/escalation.yml
tags: [escalation]
Section 2: NAPALM Integration and Advanced Device Management
NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) provides vendor-neutral network device management.
Enterprise NAPALM Framework
from napalm import get_network_driver
from napalm.base.exceptions import ConnectionException, CommandErrorException
import json
import logging
import time
from typing import Dict, List, Any, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
@dataclass
class DeviceConfig:
hostname: str
device_type: str
ip_address: str
username: str
password: str
enable_password: Optional[str] = None
timeout: int = 30
optional_args: Optional[Dict] = None
class EnterpriseNAPALMManager:
def __init__(self, config_file: str = None):
self.devices = {}
self.connections = {}
self.logger = self._setup_logging()
self.validation_engine = ConfigValidationEngine()
self.compliance_checker = ComplianceChecker()
self.change_tracker = ChangeTracker()
if config_file:
self.load_device_inventory(config_file)
def _setup_logging(self):
"""Setup comprehensive logging"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('network_automation.log'),
logging.StreamHandler()
]
)
return logging.getLogger(__name__)
def add_device(self, device_config: DeviceConfig):
"""Add device to management inventory"""
self.devices[device_config.hostname] = device_config
self.logger.info(f"Added device {device_config.hostname} to inventory")
def connect_device(self, hostname: str) -> bool:
"""Establish connection to network device"""
if hostname not in self.devices:
self.logger.error(f"Device {hostname} not found in inventory")
return False
device_config = self.devices[hostname]
try:
# Get NAPALM driver
driver = get_network_driver(device_config.device_type)
# Prepare connection parameters
connection_params = {
'hostname': device_config.ip_address,
'username': device_config.username,
'password': device_config.password,
'timeout': device_config.timeout,
'optional_args': device_config.optional_args or {}
}
# Add enable password for Cisco devices
if device_config.enable_password:
connection_params['optional_args']['secret'] = device_config.enable_password
# Create connection
device = driver(**connection_params)
device.open()
self.connections[hostname] = device
self.logger.info(f"Successfully connected to {hostname}")
return True
except ConnectionException as e:
self.logger.error(f"Failed to connect to {hostname}: {e}")
return False
except Exception as e:
self.logger.error(f"Unexpected error connecting to {hostname}: {e}")
return False
def disconnect_device(self, hostname: str):
"""Disconnect from network device"""
if hostname in self.connections:
try:
self.connections[hostname].close()
del self.connections[hostname]
self.logger.info(f"Disconnected from {hostname}")
except Exception as e:
self.logger.error(f"Error disconnecting from {hostname}: {e}")
def get_device_facts(self, hostname: str) -> Dict[str, Any]:
"""Retrieve comprehensive device facts"""
if hostname not in self.connections:
if not self.connect_device(hostname):
return {}
device = self.connections[hostname]
try:
facts = device.get_facts()
self.logger.info(f"Retrieved facts for {hostname}")
return facts
except Exception as e:
self.logger.error(f"Failed to get facts for {hostname}: {e}")
return {}
def backup_configuration(self, hostname: str, backup_dir: str = "./backups") -> bool:
"""Backup device configuration"""
if hostname not in self.connections:
if not self.connect_device(hostname):
return False
device = self.connections[hostname]
try:
# Get running configuration
config = device.get_config(retrieve='running')
# Create backup filename with timestamp
timestamp = time.strftime("%Y%m%d_%H%M%S")
backup_filename = f"{backup_dir}/{hostname}_running_{timestamp}.cfg"
# Save configuration to file
os.makedirs(backup_dir, exist_ok=True)
with open(backup_filename, 'w') as f:
f.write(config['running'])
self.logger.info(f"Configuration backed up for {hostname}: {backup_filename}")
return True
except Exception as e:
self.logger.error(f"Failed to backup configuration for {hostname}: {e}")
return False
def deploy_configuration(self, hostname: str, config_file: str,
commit: bool = False, rollback_timeout: int = 300) -> Dict[str, Any]:
"""Deploy configuration with validation and rollback capability"""
if hostname not in self.connections:
if not self.connect_device(hostname):
return {'success': False, 'error': 'Connection failed'}
device = self.connections[hostname]
result = {
'success': False,
'changes': {},
'validation_errors': [],
'rollback_performed': False
}
try:
# Read configuration file
with open(config_file, 'r') as f:
new_config = f.read()
# Pre-deployment validation
validation_result = self.validation_engine.validate_config(
hostname, new_config
)
if not validation_result['valid']:
result['validation_errors'] = validation_result['errors']
return result
# Create checkpoint for rollback
checkpoint_id = self.create_checkpoint(hostname)
# Load configuration
device.load_merge_candidate(config=new_config)
# Get diff
diff = device.compare_config()
result['changes'] = diff
if diff:
if commit:
# Commit configuration
device.commit_config()
# Post-deployment validation
validation_passed = self.post_deployment_validation(
hostname, rollback_timeout
)
if not validation_passed:
# Rollback configuration
self.rollback_to_checkpoint(hostname, checkpoint_id)
result['rollback_performed'] = True
result['success'] = False
result['error'] = 'Post-deployment validation failed'
else:
result['success'] = True
self.change_tracker.record_change(
hostname, config_file, diff
)
else:
# Discard changes (dry run)
device.discard_config()
result['success'] = True
result['dry_run'] = True
else:
result['success'] = True
result['no_changes'] = True
except Exception as e:
self.logger.error(f"Configuration deployment failed for {hostname}: {e}")
result['error'] = str(e)
# Attempt rollback on error
try:
device.discard_config()
result['rollback_performed'] = True
except:
pass
return result
def mass_deployment(self, config_map: Dict[str, str],
max_workers: int = 5, commit: bool = False) -> Dict[str, Any]:
"""Deploy configurations to multiple devices in parallel"""
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Submit deployment tasks
future_to_hostname = {
executor.submit(
self.deploy_configuration,
hostname,
config_file,
commit
): hostname
for hostname, config_file in config_map.items()
}
# Collect results
for future in as_completed(future_to_hostname):
hostname = future_to_hostname[future]
try:
result = future.result()
results[hostname] = result
if result['success']:
self.logger.info(f"Successfully deployed to {hostname}")
else:
self.logger.error(f"Deployment failed for {hostname}: {result.get('error', 'Unknown error')}")
except Exception as e:
self.logger.error(f"Exception during deployment to {hostname}: {e}")
results[hostname] = {'success': False, 'error': str(e)}
return results
def compliance_check(self, hostname: str, compliance_rules: Dict[str, Any]) -> Dict[str, Any]:
"""Perform comprehensive compliance checking"""
if hostname not in self.connections:
if not self.connect_device(hostname):
return {'compliant': False, 'error': 'Connection failed'}
device = self.connections[hostname]
try:
# Get current configuration
current_config = device.get_config(retrieve='running')['running']
# Get device facts
facts = device.get_facts()
# Perform compliance check
compliance_result = self.compliance_checker.check_compliance(
hostname, current_config, facts, compliance_rules
)
return compliance_result
except Exception as e:
self.logger.error(f"Compliance check failed for {hostname}: {e}")
return {'compliant': False, 'error': str(e)}
class ConfigValidationEngine:
"""Advanced configuration validation engine"""
def __init__(self):
self.validators = {
'syntax': SyntaxValidator(),
'security': SecurityValidator(),
'best_practices': BestPracticesValidator(),
'consistency': ConsistencyValidator()
}
def validate_config(self, hostname: str, config: str) -> Dict[str, Any]:
"""Comprehensive configuration validation"""
result = {
'valid': True,
'errors': [],
'warnings': [],
'validation_details': {}
}
for validator_name, validator in self.validators.items():
validation_result = validator.validate(hostname, config)
result['validation_details'][validator_name] = validation_result
if validation_result['errors']:
result['valid'] = False
result['errors'].extend(validation_result['errors'])
if validation_result['warnings']:
result['warnings'].extend(validation_result['warnings'])
return result
class SecurityValidator:
"""Security configuration validator"""
def __init__(self):
self.security_rules = {
'password_encryption': {
'required_lines': ['service password-encryption'],
'forbidden_lines': ['no service password-encryption']
},
'ssh_configuration': {
'required_lines': ['ip ssh version 2'],
'forbidden_lines': ['ip ssh version 1']
},
'http_server': {
'required_lines': ['no ip http server', 'no ip http secure-server'],
'forbidden_lines': ['ip http server']
},
'console_timeout': {
'pattern': r'line con 0\s+exec-timeout \d+ \d+',
'min_timeout': 5
}
}
def validate(self, hostname: str, config: str) -> Dict[str, Any]:
"""Validate security configuration"""
result = {
'errors': [],
'warnings': [],
'passed_checks': [],
'failed_checks': []
}
config_lines = config.lower().split('\n')
for rule_name, rule_config in self.security_rules.items():
check_result = self._check_security_rule(
rule_name, rule_config, config_lines
)
if check_result['passed']:
result['passed_checks'].append(rule_name)
else:
result['failed_checks'].append(rule_name)
if check_result['severity'] == 'error':
result['errors'].append(check_result['message'])
else:
result['warnings'].append(check_result['message'])
return result
def _check_security_rule(self, rule_name: str, rule_config: Dict,
config_lines: List[str]) -> Dict[str, Any]:
"""Check individual security rule"""
if 'required_lines' in rule_config:
for required_line in rule_config['required_lines']:
if not any(required_line.lower() in line for line in config_lines):
return {
'passed': False,
'severity': 'error',
'message': f"Security rule '{rule_name}': Missing required configuration '{required_line}'"
}
if 'forbidden_lines' in rule_config:
for forbidden_line in rule_config['forbidden_lines']:
if any(forbidden_line.lower() in line for line in config_lines):
return {
'passed': False,
'severity': 'error',
'message': f"Security rule '{rule_name}': Forbidden configuration found '{forbidden_line}'"
}
return {'passed': True, 'severity': 'info', 'message': f"Security rule '{rule_name}' passed"}
class ComplianceChecker:
"""Enterprise compliance checking engine"""
def check_compliance(self, hostname: str, config: str, facts: Dict,
rules: Dict[str, Any]) -> Dict[str, Any]:
"""Check device compliance against enterprise policies"""
compliance_result = {
'compliant': True,
'compliance_score': 0,
'total_checks': 0,
'passed_checks': 0,
'failed_checks': [],
'details': {}
}
for rule_category, rule_list in rules.items():
category_result = self._check_category_compliance(
hostname, config, facts, rule_category, rule_list
)
compliance_result['details'][rule_category] = category_result
compliance_result['total_checks'] += category_result['total']
compliance_result['passed_checks'] += category_result['passed']
if category_result['failed_rules']:
compliance_result['compliant'] = False
compliance_result['failed_checks'].extend(
category_result['failed_rules']
)
# Calculate compliance score
if compliance_result['total_checks'] > 0:
compliance_result['compliance_score'] = (
compliance_result['passed_checks'] /
compliance_result['total_checks'] * 100
)
return compliance_result
def _check_category_compliance(self, hostname: str, config: str, facts: Dict,
category: str, rules: List[Dict]) -> Dict[str, Any]:
"""Check compliance for a specific category"""
result = {
'total': len(rules),
'passed': 0,
'failed_rules': [],
'rule_details': {}
}
for rule in rules:
rule_result = self._evaluate_rule(hostname, config, facts, rule)
result['rule_details'][rule['name']] = rule_result
if rule_result['passed']:
result['passed'] += 1
else:
result['failed_rules'].append({
'rule': rule['name'],
'category': category,
'severity': rule.get('severity', 'medium'),
'description': rule.get('description', ''),
'remediation': rule.get('remediation', '')
})
return result
class ChangeTracker:
"""Track and audit network changes"""
def __init__(self, audit_file: str = "network_changes.log"):
self.audit_file = audit_file
self.logger = logging.getLogger(f"{__name__}.ChangeTracker")
def record_change(self, hostname: str, config_file: str, diff: str):
"""Record network change for audit trail"""
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
change_record = {
'timestamp': timestamp,
'hostname': hostname,
'config_file': config_file,
'operator': os.getenv('USER', 'unknown'),
'diff': diff,
'change_id': self._generate_change_id()
}
# Write to audit log
with open(self.audit_file, 'a') as f:
f.write(f"{json.dumps(change_record)}\n")
self.logger.info(f"Recorded change for {hostname}: {change_record['change_id']}")
def _generate_change_id(self) -> str:
"""Generate unique change identifier"""
import uuid
return str(uuid.uuid4())[:8]
This comprehensive guide demonstrates enterprise-grade network automation using Ansible and NAPALM with advanced configuration management, validation frameworks, compliance checking, and change tracking capabilities. The examples provide production-ready patterns for implementing reliable, scalable network automation in enterprise environments.