Recientemente se me ha ocurrido que para mantener un buen control de cambios, además de salvaguardar versiones anteriores y volver a un punto anterior en el tiempo, podría utilizarse GiHub.

Vale, ninguna novedad aquí, una de las funciones principales de GitHub es ésta.

Pero y si, además, aprovechamos esas capacidades para las configuraciones de nuestros dispositivos de red? En principio parece buena idea. Veamos.

Necesitamos una cuenta de GitHub, si aún no la tienes, a qué esperas?

Necesitamos alguna configuración. Establezcamos .cfg como nuestro estándar para los ficheros con las configuraciones. Por aquí de ejemplo, una configurción de Cisco ASA básica. Esta confgiuración está cortada en bastantes sitios, no esperéis que funcione, es para el ejemplo.

: Saved

: 
: Serial Number: JBE10166MUZ
: Hardware:   ASA5506, 4096 MB RAM, CPU Atom C2000 series 1250 MHz, 1 CPU (4 cores)
:
ASA Version 9.7(1)4 
!
hostname asa
xlate per-session deny tcp any4 any4
xlate per-session deny tcp any4 any6
xlate per-session deny tcp any6 any4
xlate per-session deny tcp any6 any6
xlate per-session deny udp any4 any4 eq domain
xlate per-session deny udp any4 any6 eq domain
xlate per-session deny udp any6 any4 eq domain
xlate per-session deny udp any6 any6 eq domain
names

!
interface GigabitEthernet1/1
 channel-group 1 mode on
 no nameif
 no security-level
 no ip address
!
interface GigabitEthernet1/2
 channel-group 1 mode on
 no nameif
 no security-level
 no ip address
!
interface GigabitEthernet1/3
 channel-group 2 mode on
 no nameif
 no security-level
 no ip address
!
interface GigabitEthernet1/4
 channel-group 2 mode on
 no nameif
 no security-level
 no ip address
!
interface GigabitEthernet1/5
 channel-group 3 mode on
 no nameif
 no security-level
 no ip address
!
interface GigabitEthernet1/6
 channel-group 3 mode on
 no nameif
 no security-level
 no ip address
!
interface GigabitEthernet1/7
 channel-group 4 mode on
 no nameif
 no security-level
 no ip address
!
interface GigabitEthernet1/8
 channel-group 4 mode on
 no nameif
 no security-level
 no ip address
!
interface Management1/1
 management-only
 shutdown
 no nameif
 no security-level
 no ip address
!
interface Port-channel1
 description link1
 lacp max-bundle 8
 nameif outside
 security-level 0
 ip address 192.168.1.10 255.255.255.0 
!
interface Port-channel2
 description link2
 lacp max-bundle 8
 nameif link2
 security-level 100
 ip address 192.168.10.10 255.255.255.0 
!
interface Port-channel3
 description link2
 lacp max-bundle 8
 nameif link2
 security-level 100
 ip address 192.168.20.10 255.255.255.0 
!
interface Port-channel4
 lacp max-bundle 8
 nameif link4
 security-level 50
 ip address 192.168.30.10 255.255.255.0  
!
ftp mode passive
clock timezone GMT 1
dns domain-lookup link2
dns server-group DefaultDNS
 name-server 8.8.8.8 link1 
pager lines 24
logging enable
logging timestamp
logging buffer-size 8192
logging monitor debugging
logging buffered informational
logging trap informational
logging asdm informational
logging device-id hostname
no failover
no monitor-interface service-module 
icmp unreachable rate-limit 1 burst-size 1
asdm image disk0:/asdm-771-151.bin
no asdm history enable
arp timeout 14400
no arp permit-nonconnected
arp rate-limit 16384
route homelan 0.0.0.0 0.0.0.0 192.168.1.1 1
timeout xlate 3:00:00
timeout pat-xlate 0:00:30
timeout conn 1:00:00 half-closed 0:10:00 udp 0:02:00 sctp 0:02:00 icmp 0:00:02
timeout sunrpc 0:10:00 h323 0:05:00 h225 1:00:00 mgcp 0:05:00 mgcp-pat 0:05:00
timeout sip 0:30:00 sip_media 0:02:00 sip-invite 0:03:00 sip-disconnect 0:02:00
timeout sip-provisional-media 0:02:00 uauth 0:05:00 absolute
timeout tcp-proxy-reassembly 0:01:00
timeout floating-conn 0:00:00
timeout conn-holddown 0:00:15
timeout igp stale-route 0:01:10
user-identity default-domain LOCAL
aaa authentication enable console LOCAL 
aaa authentication http console LOCAL 
aaa authentication ssh console LOCAL 
http server enable
http 192.168.1.0 255.255.192.0 link1
no snmp-server location
no snmp-server contact
service sw-reset-button
crypto ipsec security-association pmtu-aging infinite
crypto ca trustpool policy
telnet timeout 5
ssh stricthostkeycheck
ssh 192.168.1.0 255.255.255.0 link1
ssh timeout 5
ssh version 2
ssh key-exchange group dh-group1-sha1
console timeout 5
vpn-addr-assign local reuse-delay 10

threat-detection basic-threat
threat-detection statistics
threat-detection statistics tcp-intercept rate-interval 30 burst-rate 400 average-rate 200
dynamic-filter updater-client enable
dynamic-filter use-database
dynamic-filter ambiguous-is-black
ntp server 147.156.7.18
ntp server 185.132.136.116 prefer

!
class-map inspection_default
 match default-inspection-traffic
!
!
policy-map type inspect dns preset_dns_map
 parameters
  message-length maximum client auto
  message-length maximum 512
  no tcp-inspection
policy-map global_policy
 class inspection_default
  inspect ftp 
  inspect h323 h225 
  inspect h323 ras 
  inspect ip-options 
  inspect netbios 
  inspect rsh 
  inspect rtsp 
  inspect skinny  
  inspect esmtp 
  inspect sqlnet 
  inspect sunrpc 
  inspect tftp 
  inspect sip  
  inspect xdmcp 
  inspect dns preset_dns_map 
  inspect icmp 
  inspect pptp 
 class class-default
  user-statistics accounting
!
service-policy global_policy global
prompt hostname context 
no call-home reporting anonymous
hpm topN enable
Cryptochecksum:5dadcc88d501a253a006c4rf11808ff8
: end

Necesitamos Python y algunas librerías, como pyGithub. Si no lo conoces, cuando hayas acabado de crear tu cuenta de GitHub, puedes i a leerte la documentación de ésta librería. Te ayudará a entender sus capacidades, cómo se usa, y te aclarará dudas.

Ya tienes GitHub? Lo suyo es que te crees un token de CMD. Te servirá para no tener que introducir usuario y contraseña cada vez que nuestro script interactúa con GitHub. Recuerda, un script no es una persona, así que un token simplifica mucho.

Y nuestro script. No es complejo, la única dificultad que tiene es la de la librería pyGithub y su uso, que también requiere de un conocimiento básico de Git ( hash , commit , push , pull , merge , ….)

#!/usr/bin/env python3
'''
'''

__author__ = 'Aaron Castro'
__author_email__ = 'aaron.castro.sanchez@outlook.com'
__copyright__ = 'Aaron Castro'
__license__ = 'MIT'

import argparse, re
from os import listdir
from os.path import isfile, join
from github import Github

# Class to put some color on the print output
class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

# Get all our CLI arguments
def get_arguments():
    parser = argparse.ArgumentParser()
    parser.add_argument('-r', '--repo', required = True)
    parser.add_argument('-t', '--token', required = True)
    parser.add_argument('-p', '--path', default = './') # Defaults to user's home

    args = parser.parse_args()

    return(args)

# Inits GitHub repository
# * Token needed https://github.com/settings/tokens See section CMD Line
def init_git(repo, token):  
    g = Github(token)

    return(g.get_user().get_repo(repo))

# Gets all repository content file names
# * There will be one file name per local config file
def get_repo_content(repo):
    contents = repo.get_contents('/')
    file_list = []
    while len(contents) > 0:
        file = str(contents.pop(0).path)
        file_list.append(file)
    return(file_list)

# Gets all config files in the specified path
def get_local_content(path):
    return([file for file in listdir(path) if isfile(join(path, file)) and file.endswith('.cfg')])

# Pushes any new files existing locally but not in repo
def push_new_files(repo, local):
    try:
        new_files = list(set(get_local_content(local)) - set(get_repo_content(repo))) # Gets local files not in repo. Uses 2 sets from lists. Which words in list#1 are not in list #2
        for _ in new_files:
            file = open(_, 'r')
            repo.create_file(_, _, file.read())
    except:
        return(False)

    return(True)
# Updates existing repo files that also exist in the local path
def push_old_files(repo, local):
    try:
        old_files = list(set(get_local_content(local)) & set(get_repo_content(repo))) # Gets common files locally and repo. Uses 2 sets from lists. Which words in list#1 are also in list #2
        for _ in old_files:
            file = open(_, 'r')
            contents = repo.get_contents(_)
            repo.update_file(_, _, file.read(), contents.sha)
    except:
        return(False)
    
    return(True)

# Main function    
def main():
    args = get_arguments()
    if args.repo and args.token:
        # Init repository
        print('[' + bcolors.OKBLUE + 'I' + bcolors.ENDC + '] Init Github repository connection... ', end = '')
        repo = init_git(args.repo, args.token)
        if repo:
            print('[' + bcolors.OKGREEN + 'OK' + bcolors.ENDC + ']')
        else:
            print('[' + bcolors.FAIL + 'FAIL' + bcolors.ENDC + ']')
        # Pushes new config files
        print('[' + bcolors.OKBLUE + 'I' + bcolors.ENDC + '] Pushing new files... ', end = '')
        if push_new_files(repo, args.path):
            print('[' + bcolors.OKGREEN + 'OK' + bcolors.ENDC + ']')
        else:
            print('[' + bcolors.FAIL + 'FAIL' + bcolors.ENDC + ']')
        # Updates existing config files
        print('[' + bcolors.OKBLUE + 'I' + bcolors.ENDC + '] Updating existing files... ', end = '')
        if push_old_files(repo, args.path):
            print('[' + bcolors.OKGREEN + 'OK' + bcolors.ENDC + ']')
        else:
            print('[' + bcolors.FAIL + 'FAIL' + bcolors.ENDC + ']')

if __name__ == '__main__':
    main()

Ni que decir tiene que es una versión que simplemente funciona. Ni está depurada, ni espero tocarla mucho más, hace lo que tiene que hacer, con eso me es suficiente. Quien la necesite, puede usarla respetando, bueno ya sabéis, la licencia que tiene.

En vuestra mano queda obtener regularmente todas las configuraciones sobre un mismo directorio, y programar la ejecución del script. Seguramente cron podrá echaros una mano.