Header imageSkip to main content

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