Publicar cambios en Github con Python
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.