Introdução à Automação de Redes

A automação de redes é uma abordagem que utiliza software para automatizar o provisionamento, configuração, gerenciamento, teste e operação de dispositivos e serviços de rede. Com o crescimento das redes em tamanho e complexidade, a automação tornou-se essencial para garantir eficiência, consistência e redução de erros humanos.

Python se tornou a linguagem de programação preferida para automação de redes devido à sua sintaxe clara, vasta biblioteca de módulos e ampla adoção na comunidade de redes. Neste módulo, exploraremos os fundamentos da automação de redes com Python, incluindo protocolos de gerenciamento, bibliotecas populares e exemplos práticos.

Importância para Profissionais de Redes Cisco:

A automação de redes não é mais um diferencial, mas uma necessidade para profissionais de redes Cisco. Ela permite gerenciar infraestruturas complexas com menos esforço, implementar mudanças em escala, garantir consistência nas configurações e responder rapidamente a incidentes. Dominar a automação com Python é uma habilidade fundamental para avançar na carreira de redes.

Fundamentos da Automação de Redes

Por que Automatizar?

A automação de redes oferece diversos benefícios:

  • Eficiência: Tarefas que levariam horas ou dias manualmente podem ser executadas em minutos ou segundos.
  • Consistência: Elimina variações causadas por configurações manuais, garantindo que todos os dispositivos sigam os mesmos padrões.
  • Redução de erros: Minimiza erros humanos que podem causar interrupções de serviço.
  • Escalabilidade: Permite gerenciar centenas ou milhares de dispositivos com o mesmo esforço que alguns poucos.
  • Documentação automática: Scripts podem gerar documentação atualizada automaticamente.
  • Resposta rápida: Permite reagir rapidamente a incidentes ou implementar mudanças urgentes.

Níveis de Automação

A automação de redes pode ser implementada em diferentes níveis:

  1. Automação de tarefas: Scripts simples para automatizar tarefas repetitivas (ex: backup de configurações).
  2. Automação de processos: Fluxos de trabalho que combinam várias tarefas (ex: provisionamento de novos dispositivos).
  3. Automação baseada em eventos: Scripts que respondem a eventos específicos (ex: reconfiguração automática após falha).
  4. Automação baseada em intenção: Definir o estado desejado da rede, deixando o sistema determinar como alcançá-lo.

Abordagens para Automação

Existem diferentes abordagens para automatizar redes:

  • Scripts personalizados: Desenvolvimento de scripts específicos para suas necessidades.
  • Ferramentas de gerenciamento de configuração: Como Ansible, Puppet ou Chef.
  • Controladores SDN (Software-Defined Networking): Como Cisco DNA Center ou SD-WAN.
  • Plataformas de automação: Como Cisco NSO (Network Services Orchestrator).
  • APIs e bibliotecas: Uso de APIs nativas dos dispositivos e bibliotecas Python.

Neste módulo, focaremos principalmente em scripts personalizados usando Python e bibliotecas específicas para automação de redes.

Protocolos de Gerenciamento de Rede

Para automatizar redes, precisamos de protocolos que permitam a comunicação entre nossos scripts e os dispositivos de rede. Vamos explorar os principais protocolos usados na automação de redes Cisco:

SSH (Secure Shell)

SSH é um protocolo criptografado usado para acesso remoto seguro a dispositivos. É amplamente suportado e geralmente a primeira escolha para automação de redes.

  • Vantagens: Seguro, amplamente suportado, familiar para administradores de rede.
  • Desvantagens: Baseado em CLI, o que pode dificultar o parsing de saídas.
  • Uso em automação: Usado para enviar comandos CLI e coletar saídas.
  • Bibliotecas Python: Paramiko, Netmiko.

SNMP (Simple Network Management Protocol)

SNMP é um protocolo padrão para monitoramento e gerenciamento de dispositivos de rede.

  • Vantagens: Padronizado, bom para coleta de dados e monitoramento.
  • Desvantagens: Limitado para configuração, complexidade de MIBs.
  • Uso em automação: Principalmente para monitoramento e coleta de estatísticas.
  • Bibliotecas Python: PySNMP.

NETCONF (Network Configuration Protocol)

NETCONF é um protocolo baseado em XML para instalação, manipulação e remoção de configurações de dispositivos de rede.

  • Vantagens: Transacional, estruturado, suporte a rollback.
  • Desvantagens: Não disponível em todos os dispositivos ou versões de IOS.
  • Uso em automação: Configuração e monitoramento de dispositivos.
  • Bibliotecas Python: ncclient.

RESTCONF

RESTCONF é uma alternativa ao NETCONF que usa HTTP/HTTPS e formatos como JSON ou XML.

  • Vantagens: Mais simples que NETCONF, usa HTTP e JSON (familiar para desenvolvedores).
  • Desvantagens: Suporte limitado em dispositivos mais antigos.
  • Uso em automação: Configuração e monitoramento via API REST.
  • Bibliotecas Python: Requests.

gRPC/gNMI

gRPC é um framework RPC (Remote Procedure Call) de alto desempenho. gNMI (gRPC Network Management Interface) é uma implementação específica para gerenciamento de rede.

  • Vantagens: Alto desempenho, streaming, bidirecional.
  • Desvantagens: Suporte limitado em dispositivos mais antigos.
  • Uso em automação: Telemetria, configuração e operações em tempo real.
  • Bibliotecas Python: grpcio, gnmi-py.

Comparação dos Protocolos

Protocolo Transporte Formato de Dados Segurança Suporte em Dispositivos Cisco
SSH TCP Texto (CLI) Criptografado Amplo (todos os dispositivos)
SNMP UDP/TCP ASN.1 v1/v2c: Fraca, v3: Forte Amplo
NETCONF SSH XML Criptografado IOS-XE, IOS-XR, NX-OS (recentes)
RESTCONF HTTP/HTTPS JSON/XML HTTPS: Criptografado IOS-XE 16.x+, NX-OS recentes
gRPC/gNMI HTTP/2 Protobuf Criptografado IOS-XE 16.x+, IOS-XR 6.x+

Escolhendo o Protocolo Certo:

A escolha do protocolo depende de vários fatores: dispositivos disponíveis, versões de software, requisitos de segurança e tipo de automação. Para ambientes com dispositivos variados ou mais antigos, SSH é geralmente a opção mais universal. Para dispositivos mais novos, NETCONF, RESTCONF ou gRPC oferecem capacidades mais avançadas.

Bibliotecas Python para Automação de Redes

Python oferece diversas bibliotecas específicas para automação de redes. Vamos explorar as principais:

Visão Geral das Bibliotecas

  • Paramiko: Implementação de SSH em Python, base para muitas outras bibliotecas.
  • Netmiko: Simplifica o uso de SSH para dispositivos de rede, construída sobre Paramiko.
  • NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor): Fornece uma interface unificada para diferentes fabricantes.
  • ncclient: Cliente Python para NETCONF.
  • Requests: Biblioteca HTTP para APIs REST, usada com RESTCONF.
  • PySNMP: Implementação de SNMP em Python.
  • Scapy: Manipulação de pacotes de rede.
  • TextFSM/ntc-templates: Parsing de saídas CLI.
  • Pexpect: Automação de aplicativos interativos baseados em terminal.

Neste módulo, focaremos em Paramiko e NAPALM. No próximo módulo, exploraremos Netmiko em detalhes.

Instalação das Bibliotecas

A maioria das bibliotecas pode ser instalada usando pip:

# Instalando Paramiko
pip install paramiko

# Instalando NAPALM
pip install napalm

# Instalando outras bibliotecas úteis
pip install textfsm ntc-templates

Comparação das Principais Bibliotecas

Biblioteca Protocolos Fabricantes Suportados Facilidade de Uso Casos de Uso
Paramiko SSH Qualquer dispositivo com SSH Moderada Automação básica via SSH, base para outras bibliotecas
Netmiko SSH Cisco, Juniper, Arista, etc. Alta Automação via CLI, envio de comandos, configuração
NAPALM SSH, NETCONF, API Cisco IOS, IOS-XR, NX-OS, Juniper, Arista Alta Abstração multi-vendor, configuração, validação
ncclient NETCONF Dispositivos com suporte a NETCONF Moderada Operações NETCONF, configuração estruturada
Requests HTTP/HTTPS Dispositivos com API REST Alta Interação com APIs REST, RESTCONF

Automação com Paramiko (SSH)

Paramiko é uma implementação de SSH em Python que permite estabelecer conexões seguras com dispositivos de rede e executar comandos remotamente. É uma biblioteca de baixo nível que serve como base para outras bibliotecas como Netmiko.

Estabelecendo uma Conexão SSH

import paramiko
import time

def connect_ssh(hostname, username, password, port=22):
    """Estabelece uma conexão SSH com um dispositivo."""
    try:
        # Criando o cliente SSH
        ssh_client = paramiko.SSHClient()
        
        # Configurando para aceitar chaves de host desconhecidas
        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        
        # Estabelecendo a conexão
        print(f"Conectando a {hostname}...")
        ssh_client.connect(
            hostname=hostname,
            port=port,
            username=username,
            password=password,
            look_for_keys=False,
            allow_agent=False
        )
        print(f"Conectado com sucesso a {hostname}")
        
        return ssh_client
    except paramiko.AuthenticationException:
        print(f"Falha na autenticação para {hostname}")
        return None
    except paramiko.SSHException as e:
        print(f"Erro SSH para {hostname}: {str(e)}")
        return None
    except Exception as e:
        print(f"Erro ao conectar a {hostname}: {str(e)}")
        return None

# Exemplo de uso
router = {
    "hostname": "192.168.1.1",
    "username": "admin",
    "password": "cisco123"
}

# Estabelecendo a conexão
ssh_client = connect_ssh(
    hostname=router["hostname"],
    username=router["username"],
    password=router["password"]
)

Executando Comandos

def send_command(ssh_client, command):
    """Envia um comando para o dispositivo e retorna a saída."""
    try:
        # Invocando um shell
        shell = ssh_client.invoke_shell()
        
        # Enviando o comando
        print(f"Enviando comando: {command}")
        shell.send(command + "\n")
        
        # Aguardando a resposta
        time.sleep(2)  # Pausa para dar tempo ao dispositivo para responder
        
        # Lendo a saída
        output = shell.recv(65535).decode("utf-8")
        return output
    except Exception as e:
        print(f"Erro ao enviar comando: {str(e)}")
        return None

# Exemplo de uso (continuando do exemplo anterior)
if ssh_client:
    # Enviando um comando
    output = send_command(ssh_client, "show version")
    print("\nSaída do comando 'show version':")
    print(output)
    
    # Fechando a conexão
    ssh_client.close()
    print("Conexão fechada.")

Enviando Configurações

def send_config(ssh_client, config_commands):
    """Envia comandos de configuração para o dispositivo."""
    try:
        # Invocando um shell
        shell = ssh_client.invoke_shell()
        
        # Entrando no modo de configuração
        print("Entrando no modo de configuração...")
        shell.send("configure terminal\n")
        time.sleep(1)
        
        # Enviando cada comando de configuração
        for cmd in config_commands:
            print(f"Enviando: {cmd}")
            shell.send(cmd + "\n")
            time.sleep(0.5)
        
        # Saindo do modo de configuração
        shell.send("end\n")
        time.sleep(1)
        
        # Lendo a saída
        output = shell.recv(65535).decode("utf-8")
        return output
    except Exception as e:
        print(f"Erro ao enviar configuração: {str(e)}")
        return None

# Exemplo de uso
if ssh_client:
    # Configurações a serem enviadas
    config_commands = [
        "hostname Router-Paramiko",
        "interface Loopback0",
        " ip address 1.1.1.1 255.255.255.255",
        "router ospf 1",
        " network 1.1.1.1 0.0.0.0 area 0"
    ]
    
    # Enviando as configurações
    output = send_config(ssh_client, config_commands)
    print("\nSaída da configuração:")
    print(output)
    
    # Salvando a configuração
    save_output = send_command(ssh_client, "write memory")
    print("\nSalvando configuração:")
    print(save_output)
    
    # Fechando a conexão
    ssh_client.close()
    print("Conexão fechada.")

Tratamento de Erros e Boas Práticas

Ao trabalhar com Paramiko, considere as seguintes práticas:

  • Sempre feche as conexões: Use ssh_client.close() após terminar.
  • Trate exceções: Capture e trate exceções específicas como AuthenticationException e SSHException.
  • Use timeouts: Configure timeouts para evitar que seu script fique preso.
  • Considere o uso de with: Implemente um gerenciador de contexto para garantir que as conexões sejam fechadas.
  • Armazene credenciais com segurança: Evite hardcoding de senhas em seus scripts.

Limitações do Paramiko

Embora Paramiko seja poderoso, ele tem algumas limitações:

  • É uma biblioteca de baixo nível, exigindo mais código para tarefas comuns.
  • Não possui tratamento específico para diferentes tipos de dispositivos de rede.
  • O parsing de saídas CLI pode ser desafiador.
  • Não oferece recursos avançados como detecção de prompt ou tratamento de paginação.

Por essas razões, bibliotecas como Netmiko (que veremos no próximo módulo) são frequentemente preferidas para automação de redes, pois oferecem uma interface mais amigável e recursos específicos para dispositivos de rede.

Automação com NAPALM

NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor) é uma biblioteca Python que fornece uma interface unificada para interagir com diferentes dispositivos de rede, independentemente do fabricante. Ela abstrai as diferenças entre os dispositivos, permitindo que você escreva código que funcione em múltiplas plataformas.

Recursos do NAPALM

  • Abstração multi-vendor: Suporta Cisco IOS, IOS-XR, NX-OS, Juniper JunOS, Arista EOS e outros.
  • Operações de configuração: Merge (mesclagem), replace (substituição) e compare (comparação).
  • Getters: Métodos padronizados para obter informações dos dispositivos (interfaces, BGP, LLDP, etc.).
  • Validação: Verificação do estado atual do dispositivo contra um estado desejado.

Estabelecendo uma Conexão

from napalm import get_network_driver
import json

def connect_napalm(device_type, hostname, username, password, optional_args=None):
    """Estabelece uma conexão NAPALM com um dispositivo."""
    try:
        # Obtendo o driver apropriado para o tipo de dispositivo
        driver = get_network_driver(device_type)
        
        # Criando o objeto de dispositivo
        if optional_args is None:
            optional_args = {}
        
        device = driver(
            hostname=hostname,
            username=username,
            password=password,
            optional_args=optional_args
        )
        
        # Abrindo a conexão
        print(f"Conectando a {hostname} usando NAPALM...")
        device.open()
        print(f"Conectado com sucesso a {hostname}")
        
        return device
    except Exception as e:
        print(f"Erro ao conectar a {hostname}: {str(e)}")
        return None

# Exemplo de uso
router = {
    "device_type": "ios",  # Outros tipos: iosxr, nxos, junos, eos
    "hostname": "192.168.1.1",
    "username": "admin",
    "password": "cisco123",
    "optional_args": {"port": 22}
}

# Estabelecendo a conexão
device = connect_napalm(
    device_type=router["device_type"],
    hostname=router["hostname"],
    username=router["username"],
    password=router["password"],
    optional_args=router["optional_args"]
)

Obtendo Informações (Getters)

def get_device_facts(device):
    """Obtém informações básicas do dispositivo."""
    try:
        facts = device.get_facts()
        return facts
    except Exception as e:
        print(f"Erro ao obter facts: {str(e)}")
        return None

def get_device_interfaces(device):
    """Obtém informações sobre as interfaces do dispositivo."""
    try:
        interfaces = device.get_interfaces()
        return interfaces
    except Exception as e:
        print(f"Erro ao obter interfaces: {str(e)}")
        return None

# Exemplo de uso (continuando do exemplo anterior)
if device:
    # Obtendo informações básicas
    facts = get_device_facts(device)
    if facts:
        print("\nInformações do dispositivo:")
        print(f"Hostname: {facts['hostname']}")
        print(f"Modelo: {facts['model']}")
        print(f"Sistema Operacional: {facts['os_version']}")
        print(f"Interfaces: {', '.join(facts['interface_list'])}")
    
    # Obtendo informações das interfaces
    interfaces = get_device_interfaces(device)
    if interfaces:
        print("\nDetalhes das interfaces:")
        for name, details in interfaces.items():
            print(f"\nInterface: {name}")
            print(f"  Descrição: {details['description']}")
            print(f"  Status: {'Up' if details['is_up'] else 'Down'}")
            print(f"  MAC: {details['mac_address']}")
            print(f"  MTU: {details['mtu']}")
    
    # Fechando a conexão
    device.close()
    print("\nConexão fechada.")

Configurando Dispositivos

def configure_device(device, config, strategy="merge"):
    """Configura um dispositivo usando NAPALM.
    
    Args:
        device: Objeto de dispositivo NAPALM.
        config: String de configuração.
        strategy: Estratégia de configuração ('merge' ou 'replace').
    """
    try:
        print(f"\nAplicando configuração usando estratégia: {strategy}")
        
        # Carregando a configuração
        device.load_merge_candidate(config=config) if strategy == "merge" else device.load_replace_candidate(config=config)
        
        # Verificando diferenças
        print("\nDiferenças de configuração:")
        diff = device.compare_config()
        if diff:
            print(diff)
            
            # Confirmando as mudanças
            response = input("Aplicar estas mudanças? (s/n): ")
            if response.lower() == 's':
                device.commit_config()
                print("Configuração aplicada com sucesso.")
            else:
                device.discard_config()
                print("Mudanças descartadas.")
        else:
            print("Não há diferenças de configuração.")
            device.discard_config()
        
        return True
    except Exception as e:
        print(f"Erro ao configurar dispositivo: {str(e)}")
        device.discard_config()
        return False

# Exemplo de uso (continuando do exemplo anterior)
if device:
    # Configuração a ser aplicada
    config = """
interface Loopback100
 description Configurado via NAPALM
 ip address 100.100.100.100 255.255.255.255
!
router ospf 1
 network 100.100.100.100 0.0.0.0 area 0
!
"""
    
    # Aplicando a configuração
    configure_device(device, config, strategy="merge")
    
    # Fechando a conexão
    device.close()
    print("\nConexão fechada.")

Validação de Configuração

NAPALM permite validar se o estado atual do dispositivo corresponde a um estado desejado:

def validate_device(device, validation_file):
    """Valida o estado atual do dispositivo contra um estado desejado."""
    try:
        print(f"\nValidando dispositivo usando arquivo: {validation_file}")
        
        # Carregando o arquivo de validação
        with open(validation_file, 'r') as f:
            validation_data = json.load(f)
        
        # Executando a validação
        result = device.compliance_report(validation_data)
        
        # Exibindo os resultados
        if result['complies']:
            print("Validação bem-sucedida! O dispositivo está em conformidade.")
        else:
            print("Validação falhou! O dispositivo não está em conformidade:")
            print(json.dumps(result, indent=4))
        
        return result
    except Exception as e:
        print(f"Erro ao validar dispositivo: {str(e)}")
        return None

# Exemplo de arquivo de validação (validation.json)
"""
{
    "get_facts": {
        "hostname": "Router-NAPALM",
        "model": "CSR1000V"
    },
    "get_interfaces": {
        "Loopback100": {
            "is_up": true,
            "description": "Configurado via NAPALM"
        }
    }
}
"""

# Exemplo de uso (continuando do exemplo anterior)
if device:
    # Criando o arquivo de validação
    validation_file = "validation.json"
    with open(validation_file, 'w') as f:
        f.write("""
{
    "get_facts": {
        "hostname": "Router-NAPALM",
        "model": "CSR1000V"
    },
    "get_interfaces": {
        "Loopback100": {
            "is_up": true,
            "description": "Configurado via NAPALM"
        }
    }
}
""")
    
    # Validando o dispositivo
    validate_device(device, validation_file)
    
    # Fechando a conexão
    device.close()
    print("\nConexão fechada.")

Vantagens e Limitações do NAPALM

Vantagens:

  • Interface unificada para diferentes fabricantes.
  • Operações de configuração avançadas (merge, replace, compare).
  • Métodos padronizados para obter informações (getters).
  • Recursos de validação de configuração.
  • Comunidade ativa e boa documentação.

Limitações:

  • Suporte limitado a alguns fabricantes e modelos específicos.
  • Nem todos os getters estão implementados para todos os drivers.
  • Pode ser mais lento que outras abordagens para operações simples.
  • Requer configuração SSH nos dispositivos.

NAPALM vs. Netmiko:

NAPALM é ideal para ambientes multi-vendor onde você precisa de uma interface unificada e recursos avançados como validação. Netmiko é mais adequado para automação baseada em CLI, especialmente quando você precisa de interações mais específicas com o dispositivo. Em muitos casos, as duas bibliotecas podem ser usadas em conjunto: NAPALM para operações de alto nível e Netmiko para interações CLI específicas.

Exercícios Práticos

Exercício 1: Backup de Configurações

Crie um script Python que use Paramiko para se conectar a um dispositivo Cisco, execute o comando “show running-config”, e salve a saída em um arquivo com timestamp. O script deve:

  1. Aceitar hostname, username e password como argumentos.
  2. Estabelecer uma conexão SSH usando Paramiko.
  3. Enviar o comando “show running-config”.
  4. Salvar a saída em um arquivo com o formato “hostname_YYYYMMDD_HHMMSS.cfg”.
  5. Tratar erros de conexão e autenticação.

Solução do Exercício 1:

#!/usr/bin/env python3
# Script de Backup de Configurações

import paramiko
import time
import datetime
import argparse
import sys
import os

def get_args():
    """Processa os argumentos da linha de comando."""
    parser = argparse.ArgumentParser(description='Script de backup de configuração Cisco.')
    parser.add_argument('-H', '--hostname', required=True, help='Hostname ou IP do dispositivo')
    parser.add_argument('-u', '--username', required=True, help='Nome de usuário')
    parser.add_argument('-p', '--password', required=True, help='Senha')
    parser.add_argument('-d', '--directory', default='.', help='Diretório para salvar o backup')
    return parser.parse_args()

def connect_ssh(hostname, username, password, port=22):
    """Estabelece uma conexão SSH com um dispositivo."""
    try:
        # Criando o cliente SSH
        ssh_client = paramiko.SSHClient()
        
        # Configurando para aceitar chaves de host desconhecidas
        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        
        # Estabelecendo a conexão
        print(f"Conectando a {hostname}...")
        ssh_client.connect(
            hostname=hostname,
            port=port,
            username=username,
            password=password,
            look_for_keys=False,
            allow_agent=False
        )
        print(f"Conectado com sucesso a {hostname}")
        
        return ssh_client
    except paramiko.AuthenticationException:
        print(f"Falha na autenticação para {hostname}")
        return None
    except paramiko.SSHException as e:
        print(f"Erro SSH para {hostname}: {str(e)}")
        return None
    except Exception as e:
        print(f"Erro ao conectar a {hostname}: {str(e)}")
        return None

def get_device_hostname(ssh_client):
    """Obtém o hostname configurado no dispositivo."""
    try:
        # Invocando um shell
        shell = ssh_client.invoke_shell()
        
        # Enviando o comando
        shell.send("show running-config | include hostname\n")
        
        # Aguardando a resposta
        time.sleep(2)
        
        # Lendo a saída
        output = shell.recv(65535).decode("utf-8")
        
        # Extraindo o hostname
        for line in output.splitlines():
            if "hostname" in line:
                parts = line.split()
                if len(parts) >= 2:
                    return parts[1]
        
        return "unknown"
    except Exception as e:
        print(f"Erro ao obter hostname: {str(e)}")
        return "unknown"

def get_running_config(ssh_client):
    """Obtém a configuração em execução do dispositivo."""
    try:
        # Invocando um shell
        shell = ssh_client.invoke_shell()
        
        # Enviando o comando
        print("Executando 'show running-config'...")
        shell.send("terminal length 0\n")  # Desativa paginação
        time.sleep(1)
        shell.send("show running-config\n")
        
        # Aguardando a resposta (configurações podem ser grandes)
        time.sleep(10)
        
        # Lendo a saída
        output = shell.recv(65535).decode("utf-8")
        return output
    except Exception as e:
        print(f"Erro ao obter configuração: {str(e)}")
        return None

def save_config_to_file(config, hostname, directory):
    """Salva a configuração em um arquivo com timestamp."""
    try:
        # Criando o timestamp
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        
        # Criando o nome do arquivo
        filename = f"{hostname}_{timestamp}.cfg"
        filepath = os.path.join(directory, filename)
        
        # Salvando a configuração
        with open(filepath, 'w') as f:
            f.write(config)
        
        print(f"Configuração salva em: {filepath}")
        return filepath
    except Exception as e:
        print(f"Erro ao salvar configuração: {str(e)}")
        return None

def main():
    """Função principal."""
    # Obtendo argumentos
    args = get_args()
    
    # Verificando se o diretório existe
    if not os.path.exists(args.directory):
        try:
            os.makedirs(args.directory)
            print(f"Diretório criado: {args.directory}")
        except Exception as e:
            print(f"Erro ao criar diretório: {str(e)}")
            sys.exit(1)
    
    # Estabelecendo a conexão
    ssh_client = connect_ssh(
        hostname=args.hostname,
        username=args.username,
        password=args.password
    )
    
    if not ssh_client:
        print("Não foi possível estabelecer conexão SSH. Saindo.")
        sys.exit(1)
    
    try:
        # Obtendo o hostname configurado
        device_hostname = get_device_hostname(ssh_client)
        print(f"Hostname do dispositivo: {device_hostname}")
        
        # Obtendo a configuração
        config = get_running_config(ssh_client)
        if not config:
            print("Não foi possível obter a configuração. Saindo.")
            ssh_client.close()
            sys.exit(1)
        
        # Salvando a configuração
        save_config_to_file(config, device_hostname, args.directory)
        
    finally:
        # Fechando a conexão
        ssh_client.close()
        print("Conexão fechada.")

if __name__ == "__main__":
    main()

Exercício 2: Inventário de Rede com NAPALM

Crie um script Python que use NAPALM para coletar informações de um dispositivo Cisco e gere um relatório de inventário em formato JSON. O script deve:

  1. Conectar-se ao dispositivo usando NAPALM.
  2. Coletar informações básicas (facts), interfaces, e tabela ARP.
  3. Organizar as informações em um dicionário estruturado.
  4. Salvar o dicionário em um arquivo JSON formatado.

Solução do Exercício 2:

#!/usr/bin/env python3
# Inventário de Rede com NAPALM

from napalm import get_network_driver
import json
import argparse
import sys
import os
from datetime import datetime

def get_args():
    """Processa os argumentos da linha de comando."""
    parser = argparse.ArgumentParser(description='Script de inventário de rede com NAPALM.')
    parser.add_argument('-d', '--device_type', required=True, choices=['ios', 'iosxr', 'nxos', 'junos', 'eos'],
                        help='Tipo de dispositivo (ios, iosxr, nxos, junos, eos)')
    parser.add_argument('-H', '--hostname', required=True, help='Hostname ou IP do dispositivo')
    parser.add_argument('-u', '--username', required=True, help='Nome de usuário')
    parser.add_argument('-p', '--password', required=True, help='Senha')
    parser.add_argument('-P', '--port', type=int, default=22, help='Porta SSH (padrão: 22)')
    parser.add_argument('-o', '--output_dir', default='.', help='Diretório para salvar o relatório')
    return parser.parse_args()

def connect_napalm(device_type, hostname, username, password, port=22):
    """Estabelece uma conexão NAPALM com um dispositivo."""
    try:
        # Obtendo o driver apropriado para o tipo de dispositivo
        driver = get_network_driver(device_type)
        
        # Criando o objeto de dispositivo
        optional_args = {'port': port}
        
        device = driver(
            hostname=hostname,
            username=username,
            password=password,
            optional_args=optional_args
        )
        
        # Abrindo a conexão
        print(f"Conectando a {hostname} usando NAPALM ({device_type})...")
        device.open()
        print(f"Conectado com sucesso a {hostname}")
        
        return device
    except Exception as e:
        print(f"Erro ao conectar a {hostname}: {str(e)}")
        return None

def collect_device_info(device):
    """Coleta informações do dispositivo usando NAPALM."""
    inventory = {}
    
    try:
        # Coletando informações básicas
        print("Coletando informações básicas...")
        inventory['facts'] = device.get_facts()
        
        # Coletando informações das interfaces
        print("Coletando informações das interfaces...")
        inventory['interfaces'] = device.get_interfaces()
        inventory['interfaces_counters'] = device.get_interfaces_counters()
        
        # Coletando informações de IP
        print("Coletando informações de IP...")
        inventory['interfaces_ip'] = device.get_interfaces_ip()
        
        # Coletando tabela ARP
        print("Coletando tabela ARP...")
        inventory['arp_table'] = device.get_arp_table()
        
        # Coletando tabela MAC
        print("Coletando tabela MAC...")
        try:
            inventory['mac_address_table'] = device.get_mac_address_table()
        except NotImplementedError:
            print("Aviso: get_mac_address_table não implementado para este dispositivo.")
            inventory['mac_address_table'] = []
        
        # Coletando informações de LLDP
        print("Coletando informações de LLDP...")
        try:
            inventory['lldp_neighbors'] = device.get_lldp_neighbors()
            inventory['lldp_neighbors_detail'] = device.get_lldp_neighbors_detail()
        except NotImplementedError:
            print("Aviso: LLDP getters não implementados para este dispositivo.")
            inventory['lldp_neighbors'] = {}
            inventory['lldp_neighbors_detail'] = {}
        
        # Adicionando metadados
        inventory['metadata'] = {
            'collected_at': datetime.now().isoformat(),
            'device_type': device.platform,
            'hostname': inventory['facts'].get('hostname', 'unknown')
        }
        
        return inventory
    except Exception as e:
        print(f"Erro ao coletar informações: {str(e)}")
        return None

def save_inventory_to_json(inventory, output_dir):
    """Salva o inventário em um arquivo JSON."""
    try:
        # Criando o nome do arquivo
        hostname = inventory['facts'].get('hostname', 'unknown')
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"{hostname}_inventory_{timestamp}.json"
        filepath = os.path.join(output_dir, filename)
        
        # Salvando o inventário
        with open(filepath, 'w') as f:
            json.dump(inventory, f, indent=4)
        
        print(f"Inventário salvo em: {filepath}")
        return filepath
    except Exception as e:
        print(f"Erro ao salvar inventário: {str(e)}")
        return None

def main():
    """Função principal."""
    # Obtendo argumentos
    args = get_args()
    
    # Verificando se o diretório de saída existe
    if not os.path.exists(args.output_dir):
        try:
            os.makedirs(args.output_dir)
            print(f"Diretório criado: {args.output_dir}")
        except Exception as e:
            print(f"Erro ao criar diretório: {str(e)}")
            sys.exit(1)
    
    # Estabelecendo a conexão
    device = connect_napalm(
        device_type=args.device_type,
        hostname=args.hostname,
        username=args.username,
        password=args.password,
        port=args.port
    )
    
    if not device:
        print("Não foi possível estabelecer conexão NAPALM. Saindo.")
        sys.exit(1)
    
    try:
        # Coletando informações
        inventory = collect_device_info(device)
        if not inventory:
            print("Não foi possível coletar informações. Saindo.")
            device.close()
            sys.exit(1)
        
        # Salvando o inventário
        save_inventory_to_json(inventory, args.output_dir)
        
    finally:
        # Fechando a conexão
        device.close()
        print("Conexão fechada.")

if __name__ == "__main__":
    main()

Exercício 3: Validação de Configuração

Crie um script Python que use NAPALM para validar a configuração de um dispositivo Cisco contra um estado desejado. O script deve:

  1. Ler um arquivo JSON contendo o estado desejado (interfaces, rotas, etc.).
  2. Conectar-se ao dispositivo usando NAPALM.
  3. Executar a validação usando o método compliance_report.
  4. Gerar um relatório detalhado dos resultados da validação.
  5. Salvar o relatório em um arquivo JSON.

Solução do Exercício 3:

#!/usr/bin/env python3
# Validação de Configuração com NAPALM

from napalm import get_network_driver
import json
import argparse
import sys
import os
from datetime import datetime

def get_args():
    """Processa os argumentos da linha de comando."""
    parser = argparse.ArgumentParser(description='Script de validação de configuração com NAPALM.')
    parser.add_argument('-d', '--device_type', required=True, choices=['ios', 'iosxr', 'nxos', 'junos', 'eos'],
                        help='Tipo de dispositivo (ios, iosxr, nxos, junos, eos)')
    parser.add_argument('-H', '--hostname', required=True, help='Hostname ou IP do dispositivo')
    parser.add_argument('-u', '--username', required=True, help='Nome de usuário')
    parser.add_argument('-p', '--password', required=True, help='Senha')
    parser.add_argument('-P', '--port', type=int, default=22, help='Porta SSH (padrão: 22)')
    parser.add_argument('-v', '--validation_file', required=True, help='Arquivo JSON de validação')
    parser.add_argument('-o', '--output_dir', default='.', help='Diretório para salvar o relatório')
    return parser.parse_args()

def connect_napalm(device_type, hostname, username, password, port=22):
    """Estabelece uma conexão NAPALM com um dispositivo."""
    try:
        # Obtendo o driver apropriado para o tipo de dispositivo
        driver = get_network_driver(device_type)
        
        # Criando o objeto de dispositivo
        optional_args = {'port': port}
        
        device = driver(
            hostname=hostname,
            username=username,
            password=password,
            optional_args=optional_args
        )
        
        # Abrindo a conexão
        print(f"Conectando a {hostname} usando NAPALM ({device_type})...")
        device.open()
        print(f"Conectado com sucesso a {hostname}")
        
        return device
    except Exception as e:
        print(f"Erro ao conectar a {hostname}: {str(e)}")
        return None

def load_validation_file(validation_file):
    """Carrega o arquivo de validação JSON."""
    try:
        with open(validation_file, 'r') as f:
            validation_data = json.load(f)
        
        print(f"Arquivo de validação carregado: {validation_file}")
        return validation_data
    except FileNotFoundError:
        print(f"Erro: Arquivo de validação não encontrado: {validation_file}")
        return None
    except json.JSONDecodeError as e:
        print(f"Erro: Arquivo de validação contém JSON inválido: {str(e)}")
        return None
    except Exception as e:
        print(f"Erro ao carregar arquivo de validação: {str(e)}")
        return None

def validate_device(device, validation_data):
    """Valida o dispositivo contra o estado desejado."""
    try:
        print("Executando validação...")
        result = device.compliance_report(validation_data)
        
        # Exibindo resumo dos resultados
        if result.get('complies', False):
            print("Validação bem-sucedida! O dispositivo está em conformidade.")
        else:
            print("Validação falhou! O dispositivo não está em conformidade.")
            
            # Contando falhas
            failure_count = 0
            for getter, getter_result in result.items():
                if getter != 'complies' and isinstance(getter_result, dict) and not getter_result.get('complies', True):
                    failure_count += 1
            
            print(f"Total de falhas: {failure_count}")
        
        return result
    except Exception as e:
        print(f"Erro ao validar dispositivo: {str(e)}")
        return None

def save_validation_report(report, hostname, output_dir):
    """Salva o relatório de validação em um arquivo JSON."""
    try:
        # Criando o nome do arquivo
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"{hostname}_validation_{timestamp}.json"
        filepath = os.path.join(output_dir, filename)
        
        # Adicionando metadados ao relatório
        report_with_metadata = {
            'metadata': {
                'hostname': hostname,
                'timestamp': datetime.now().isoformat(),
                'overall_compliance': report.get('complies', False)
            },
            'report': report
        }
        
        # Salvando o relatório
        with open(filepath, 'w') as f:
            json.dump(report_with_metadata, f, indent=4)
        
        print(f"Relatório de validação salvo em: {filepath}")
        return filepath
    except Exception as e:
        print(f"Erro ao salvar relatório: {str(e)}")
        return None

def main():
    """Função principal."""
    # Obtendo argumentos
    args = get_args()
    
    # Verificando se o diretório de saída existe
    if not os.path.exists(args.output_dir):
        try:
            os.makedirs(args.output_dir)
            print(f"Diretório criado: {args.output_dir}")
        except Exception as e:
            print(f"Erro ao criar diretório: {str(e)}")
            sys.exit(1)
    
    # Carregando o arquivo de validação
    validation_data = load_validation_file(args.validation_file)
    if not validation_data:
        print("Não foi possível carregar o arquivo de validação. Saindo.")
        sys.exit(1)
    
    # Estabelecendo a conexão
    device = connect_napalm(
        device_type=args.device_type,
        hostname=args.hostname,
        username=args.username,
        password=args.password,
        port=args.port
    )
    
    if not device:
        print("Não foi possível estabelecer conexão NAPALM. Saindo.")
        sys.exit(1)
    
    try:
        # Executando a validação
        validation_report = validate_device(device, validation_data)
        if validation_report is None:
            print("Não foi possível executar a validação. Saindo.")
            device.close()
            sys.exit(1)
        
        # Salvando o relatório
        save_validation_report(validation_report, args.hostname, args.output_dir)
        
    finally:
        # Fechando a conexão
        device.close()
        print("Conexão fechada.")

if __name__ == "__main__":
    main()

Exemplo de arquivo de validação (validation.json):

{
    "get_facts": {
        "os_version": "15.4",
        "hostname": "Router1",
        "model": "CSR1000V"
    },
    "get_interfaces": {
        "GigabitEthernet1": {
            "is_up": true,
            "is_enabled": true
        },
        "Loopback0": {
            "is_up": true,
            "description": "Management Interface"
        }
    },
    "get_interfaces_ip": {
        "GigabitEthernet1": {
            "ipv4": {
                "192.168.1.1": {
                    "prefix_length": 24
                }
            }
        },
        "Loopback0": {
            "ipv4": {
                "1.1.1.1": {
                    "prefix_length": 32
                }
            }
        }
    }
}

Avaliação do Módulo

Quiz de Conhecimentos

Responda às seguintes perguntas para testar seu conhecimento sobre os conceitos apresentados neste módulo:

  1. Quais são os principais benefícios da automação de redes?
  2. Qual protocolo é mais adequado para automação em dispositivos Cisco mais antigos?
  3. Qual é a principal diferença entre NETCONF e RESTCONF?
  4. Qual biblioteca Python é uma implementação de SSH usada como base para outras bibliotecas de automação?
  5. Qual é a principal vantagem do NAPALM em relação ao Paramiko?
  6. Quais são as três operações de configuração suportadas pelo NAPALM?
  7. O que são “getters” no contexto do NAPALM?
  8. Qual método do NAPALM é usado para validar o estado atual de um dispositivo contra um estado desejado?
  9. Por que é importante usar ssh_client.close() após terminar de usar uma conexão Paramiko?
  10. Qual é a principal limitação do Paramiko para automação de redes em comparação com bibliotecas como Netmiko?

Respostas do Quiz:

  1. Benefícios da automação de redes: Eficiência, consistência, redução de erros humanos, escalabilidade, documentação automática e resposta rápida a incidentes.
  2. Protocolo para dispositivos antigos: SSH é o mais adequado para automação em dispositivos Cisco mais antigos, pois é amplamente suportado em todas as versões do IOS.
  3. Diferença entre NETCONF e RESTCONF: NETCONF usa SSH e XML, enquanto RESTCONF usa HTTP/HTTPS e JSON/XML. RESTCONF é geralmente considerado mais simples e mais familiar para desenvolvedores web.
  4. Biblioteca SSH base: Paramiko é a implementação de SSH em Python usada como base para outras bibliotecas como Netmiko.
  5. Vantagem do NAPALM: A principal vantagem do NAPALM é a abstração multi-vendor, que fornece uma interface unificada para interagir com dispositivos de diferentes fabricantes.
  6. Operações de configuração do NAPALM: Merge (mesclagem), replace (substituição) e compare (comparação).
  7. Getters no NAPALM: São métodos padronizados para obter informações dos dispositivos, como get_facts(), get_interfaces(), get_arp_table(), etc.
  8. Método de validação do NAPALM: O método compliance_report() é usado para validar o estado atual de um dispositivo contra um estado desejado.
  9. Importância de ssh_client.close(): Para liberar recursos do sistema, garantir que todos os dados sejam enviados e evitar que o arquivo fique bloqueado para outros processos.
  10. Limitação do Paramiko: Paramiko é uma biblioteca de baixo nível que não possui tratamento específico para diferentes tipos de dispositivos de rede, não oferece recursos como detecção de prompt ou tratamento de paginação, e requer mais código para tarefas comuns.

Projeto Prático

Desenvolva um sistema de gerenciamento de configuração para dispositivos Cisco que combine Paramiko e NAPALM. O sistema deve:

  1. Ler uma lista de dispositivos de um arquivo CSV (hostname, IP, tipo, credenciais).
  2. Oferecer um menu com opções para:
    • Fazer backup da configuração de todos os dispositivos.
    • Coletar e exibir informações básicas de todos os dispositivos.
    • Aplicar uma configuração a um ou mais dispositivos.
    • Validar a configuração de um dispositivo contra um estado desejado.
  3. Usar Paramiko para operações de backup e configuração básica.
  4. Usar NAPALM para coleta de informações e validação.
  5. Gerar logs detalhados de todas as operações.
  6. Implementar tratamento de erros robusto.

Organize o código em funções reutilizáveis e documente adequadamente.

Referências e Recursos Adicionais

Documentação Oficial

Livros Recomendados

  • Kirk Byers – “Python for Network Engineers”
  • Jason Edelman, Scott S. Lowe, Matt Oswalt – “Network Programmability and Automation”
  • Eric Chou – “Mastering Python Networking”

Recursos Online

Cursos e Treinamentos

Comunidades e Fóruns