Use Python for Git
It has recently occurred to me that to maintain good change control, in addition to safeguarding previous versions and going back to an earlier point in time, GiHub could be used.
Ok, no news here, one of the main functions of GitHub is this.
But what if, in addition, we take advantage of these capabilities for the configurations of our network devices? In principle it seems like a good idea. Let’s see.
We need a GitHub account, if you don’t have one yet, what are you waiting for?
We need some configuration. Let’s set .cfg as our standard for configuration files. Here is an example of a basic Cisco ASA configuration. This configuration is cut in many places, don’t expect it to work, it is for example.
: 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
We need Python and some libraries, such as pyGithub. If you don’t know it, when you have finished creating your GitHub account, you can read the documentation of this library. It will help you understand its capabilities, how to use it, and it will clarify your doubts.
Already have GitHub? The best thing to do is to create a CMD token. It will help you to avoid having to enter username and password every time our script interacts with GitHub. Remember, a script is not a person, so a token simplifies a lot.
And our script. It is not complex, the only difficulty it has is the pyGithub library and its use, which also requires a basic knowledge of 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()
Needless to say, it is a version that just works. It is not debugged, nor do I expect to touch it much more, it does what it has to do, that’s enough for me. Whoever needs it, can use it respecting, well you know, the license it has.
It is up to you to regularly get all the configurations on the same directory, and to schedule the execution of the script. Surely cron will be able to give you a hand.