Backing up and archiving Cisco IOS/ASA configurations live from the router without TFTP
In Python and with Subversion archiving
At some point as a network admin, I did find easier to parse config file using python expect and some regexp, rather than relying on the unreliable TFTP. Here's a script which worked fine for one year or so. It also does automate archiving in subversion. It's delibaretly in a shape suitable for a blog page, working after tuning two paths in the first script, but without a tarball and 12 pages of doco. Python is simple enough to read and understand even for peoples having never learned this language.
First of two source files: ciscobak.py:
#!/usr/bin/env python # # simpler rancid than rancid, with subversion archiving # Philippe Strauss, philou at philou.ch, 2008 # from configtree import Config import sys, types, re from pprint import pprint import os from time import strftime import shutil, getopt, time import pexpect DEBUG=False # directory where we write the routers config files write_basedir = "/some/path/for/last/rtr-conf" # svn repository svn_repos = "file:///some/path/for/svn/rtr-conf" def notdefined(user): if ((user == "") or (user == None)): return True # where the bulk of the work is done def fetch_conf(ip_address, devtype, filehandle, proto, password, user=None, enable=None): if proto == "telnet": dev = pexpect.spawn("telnet %s" % ip_address) if DEBUG: dev.logfile = sys.stdout if not notdefined(user): dev.expect("sername: ") dev.sendline(user) dev.expect("assword: ") dev.sendline(password) elif proto == "ssh": if not notdefined(user): dev = pexpect.spawn("ssh %s@%s" % (user, ip_address)) # should check for conn refused else: dev = pexpect.spawn("ssh %s" % ip_address) if DEBUG: dev.logfile = sys.stdout # unregistered host key: # Are you sure you want to continue connecting (yes/no)? i = dev.expect([pexpect.TIMEOUT, "sure you want to continue connecting", "assword: "]) if (i == 0): print 'SSH could not login. Here is what SSH said:' print dev.before, dev.after elif (i == 1): dev.sendline("yes") j = dev.expect([pexpect.TIMEOUT, 'assword: ']) if (j == 0): print 'SSH could not login. Here is what SSH said:' print dev.before, dev.after dev.sendline(password) i = dev.expect([pexpect.TIMEOUT, ">", "#"]) if (i == 0): print 'Cisco user or admin prompt not found:' print dev.before, dev.after tmpf = os.tmpfile() dev.logfile = tmpf if (i == 1): dev.sendline("enable") dev.expect("assword: ") if enable != None: dev.sendline(enable) else: dev.sendline(password) dev.expect("#") if (devtype == "cisco-ios"): dev.sendline("term len 0") dev.expect("#") elif (devtype == "cisco-fw7"): dev.sendline("term pager 0") dev.expect("#") elif (devtype == "cisco-fw6"): dev.sendline("pager 0") dev.expect("#") dev.sendline("show running") dev.expect("#") # too simple: may be used in ACL remark. .*#$ time.sleep(1) dev.sendline("exit") dev.expect(pexpect.EOF) if (devtype == "cisco-ios"): reBegin = re.compile(r"Current\ configuration\ *:") reEnd = re.compile(r"end") if (devtype == "cisco-fw7"): reBegin = re.compile(r"ASA Version") reEnd = re.compile(r"Cryptochecksum\:") if (devtype == "cisco-fw6"): reBegin = re.compile(r"PIX Version") reEnd = re.compile(r"Cryptochecksum\:") tmpf.seek(0) buf = tmpf.readlines() idx = 0; idxBegin = None; idxEnd = None for line in buf: if reBegin.match(line): idxBegin = idx if reEnd.match(line): idxEnd = idx idx += 1 if ((idxBegin == None) or (idxEnd == None)): print "Config cleanup failed! idxBegin=%s, idxEnd=%s" % (idxBegin, idxEnd) # remove garbage from the sh run output reEndLine = re.compile('\r\n') reClockP = re.compile(r'^ntp\ clock-period.*') reCryptoCSum = re.compile(r'Cryptochecksum:.*') clean = [] for i in range(idxBegin+1, idxEnd+1): # remove \r\n line = reEndLine.sub('', buf[i]) if devtype == "cisco-ios": # skip "ntp clock-period" line if reClockP.match(line): continue if devtype == "cisco-fw7": # skip "Cryptochecksum:" line if reCryptoCSum.match(line): continue clean.append("%s\n" % line) tmpf.close() dev.close() filehandle.writelines(clean) def usage(): print """ ciscobak.py [options] -d host : hostname or ip address of device -t device-type : one of cisco-ios or cisco-fw7 -r protocol : either ssh or telnet -u username -p password """ if __name__ == "__main__": # arguments parsing try: opts, args = getopt.getopt(sys.argv[1:], "d:t:r:u:p:e:", ["device=", "type=", "protocol=", "username=", "password=", "enable="]) except getopt.GetoptError, err: # print help information and exit: print str(err) # will print something like "option -a not recognized" usage() sys.exit(2) device = None; devtype = None; proto = None; username = None; password = None; enable = None for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() elif o in ("-d"): device = a elif o in ("-t"): devtype = a elif o in ("-r"): proto = a elif o in ("-u"): username = a elif o in ("-p"): password = a elif o in ("-e"): enable = a else: assert False, "unhandled option" # main # first: config tree mode, automated, using svn if ((device == None) and (devtype == None) and (proto == None) and (username == None) and (password == None) and (enable == None)): work_dir = write_basedir + "/cbak" ret = shutil.rmtree(work_dir, ignore_errors=True) ret = os.system("svn checkout %s %s" % (svn_repos, work_dir)) if (ret != 0): print "svn checkout failed!" exit(1) # get a list of device to poll conf = Config() first_time = [] for dic in conf.list(): fname = dic['path'][-1] + ".txt" fpath = work_dir + "/" + fname try: os.unlink(fpath) except OSError: print "\nNo previous file: %s\n" % fpath # for svn import first_time.append(fname) outf = open(fpath, mode='w+') print "scraping %s; %s..." % (dic['path'][-1], dic['address']) try: enable = dic['enable'] except KeyError: enable = None fetch_conf(dic['address'], dic['devtype'], outf, dic['proto'], dic['password'], dic['username'], enable) outf.close time_str = strftime("%Y-%m-%d_%Hh%M") if len(first_time) > 0: for new in first_time: full_svn = svn_repos + "/" + new ret = os.system("cd %s; svn import -m %s %s %s" % (work_dir, time_str, new, full_svn)) if (ret != 0): print "svn import %s failed!" % new exit(1) ret = os.system("cd %s; svn commit -m %s" % (work_dir, time_str)) if (ret != 0): print "svn commit failed!" exit(1) elif ((device != None) and (devtype != None) and (proto != None) and (username != None) and (password != None)): # manual mode fetch_conf(device, devtype, sys.stdout, proto, password, username, enable) else: print "wrong number of argument given: no argument mean run in automated mode," print "otherwise you must give 5 arguments."
The second of two source files: configtree.py. Run it as itself to understand the tree traversal:
#!/usr/bin/env python # # flexible yet simple tree config # Philippe Strauss, philou at philou.ch, 2008 # import types, re from pprint import pprint DEBUG=False # class encapsulating a config tree and processing method to traverse it class Config: tree = { 'ios':{ 'devtype':'cisco-ios', 'proto':'telnet', 'infra':{ 'username':'kiko', 'password':'blurb', 'rt-cc-b1':{'address':'195.x.x.x'}, 'rt-cc-b2':{'address':'195.x.x.x'}, }, 'customers':{ 'username':'bofh', 'password':'rtfm', 'gw-truc':{'address':'195.x.x.x'}, }, }, 'asa':{ 'devtype':'cisco-fw7', 'proto':'ssh', 'infra':{ 'username':'root', 'password':'wrongpwd', 'fw-bidule':{'address':'195.x.x.x'}, # TODO: context system }, } } # here be dragons: config part ended, this is code # you should not fiddle too much with def _sortDictLast(self, dict): lDict=[]; lString=[] for k in dict.keys(): if isinstance(dict[k], types.DictType): lDict.append(k) else: lString.append(k) lString.extend(lDict) return lString # recursive def _list(self, dict, path0=[], attribute0={}): attribute = attribute0.copy() end = True nodek = self._sortDictLast(dict) for k in nodek: path = [] path.extend(path0) if isinstance(dict[k], types.DictType): end = False path.append(k) self._list(dict[k], path, attribute) else: attribute[k] = dict[k] if end: attribute['path'] = path0 self.ret.append(attribute) return self.ret def list(self): self.ret = [] return self._list(self.tree) def _simpleTree(self, inDict, outDict): for k in inDict.keys(): if isinstance(inDict[k], types.DictType): if hasSubDict(inDict[k]): outDict[k] = {} # recurse self._simpleTree(inDict[k], outDict[k]) else: # use the keys as a leaf tag outDict[k] = {} def simpleTree(self): simpler = {} self._simpleTree(self.tree, simpler) return simpler def _attributeTree(self, inDict, outDict, path0=[]): for k in inDict.keys(): if isinstance(inDict[k], types.DictType): path = [] path.extend(path0) if hasSubDict(inDict[k]): outDict[k] = {} path.append(k) # recurse self._attributeTree(inDict[k], outDict[k], path) else: # use the key as a leaf tag outDict[k] = inDict[k] outDict[k]['path'] = path def attributeTree(self): attrib = {} self._attributeTree(self.tree, attrib) return attrib # tree leaf: return True if dictionary contains any sub-dictionary def hasSubDict(dict): sub = False for k in dict.keys(): if type(dict[k]) == types.DictType: sub = True return sub if __name__ == "__main__": conf = Config() pprint(conf.list())
Comments