2.04.2013

Cisco VOIP phone device information gathering

Our network team needed to true-up maintenance support, and validate inventory for our 6000+ Cisco IP Phones.


So I wrote a python script to provide the following information, via switch CDP discovery and dumping the phone's device details via http from the phones themselves:
  • Switch, Port
  • PhoneName
  • IP
  • Platform(From CDP)
  • SerialNumber
  • MACAddress
  • ModelNumber 

In a nutshell, the script performs the following:
  • logs into each of the access switches provided (fed with file input, one per line) 
  • via CDP neighbor discovery grabs all devices named SEP* (standard Cisco convention)
  • from that list, runs "cdp neighbor <int> detail" to get Platform and IP info for each discovered phone
  • from the IP information, grabs the devices extended information via http (xml output)
  • parses the xml and returns output as a csv file suitable for importing to a spreadsheet



#!/usr/bin/env python
"""
Mike Orstead
01/2013
http://stewpid-litterbox.blogspot.com/

Utility script to get Cisco IP phone device information through a combination 

of Cisco switch info (CDP) and http grabs from the device themselves (xml).

Pre-reqs:

-switch access with a common username/password.  User needs ability to run 
cdp neighbor discover and "term len 0" (so command output is not run 
through a pager) 
-Update inputfile(s) below with site specific info (or modify to accept file location via user input)

NOTES and WARNINGS:-This script instance assumes that the CDP discover finds phones with 

device names SEP*, script may be easily modified for other conventions.
-We have Cisco Video Conferencing devs that need to be handled, as IP address 
information is not returned with CDP (CTS_CODEC). Other devices may need to 
be handled similarly in your environment. Search reCODEC in the code below.


"""

# Import Python Modules
import os, datetime
import sys
import pexpect
import re
import getpass
import urllib
from lxml import etree

date = datetime.datetime.now()

datestamp = date.strftime("%Y%m%d") 

def main():

    source_choices = 'Run script against Site1 or Site2 switches? '
    while True:
        source_answer = raw_input(source_choices).lower()
        if source_answer == 'site1':
            source = 'Site1'
            break
        elif source_answer =='site2':
            source = 'Site2'
            break
        else:
            print "Invalid choice" + "\n"

    userName = raw_input('Username: ')

    userPassword = getpass.getpass('Password: ')

    opspath = '/example-path/'

    outputpath = opspath+'CIPphoneinfo_Output/'

    # get the switches from proper inputfile

    if source == 'Site1':
        # TODO: should have a 'try' here
        inputfile = open(opspath+'Site2_ACCESS_SWITCHES.txt')
        ofile = 'cipinfo_Output_Site1.' + datestamp + '.csv'
        outputfile = file(outputpath+ofile,'w')
    elif source == 'Site2':
        inputfile = open(opspath+'Site2_ACCESS_SWITCHES.txt')
        ofile = 'cipinfo_Output_Site2.' + datestamp + '.csv'
        outputfile = file(outputpath+ofile,'w')

    output = {}

    for hostname in inputfile:
        phonelist = []
        hostname = hostname.rstrip('\n')
        logfile = sys.stdout
        tmpf = os.tmpfile()
        print 'Connecting to switch: ' +  hostname + ' and discovering phones'
        # TODO:  should catch a TIMEOUT here in the pexpect
        router = pexpect.spawn('ssh '+userName+'@'+hostname)
        router.expect('word:')
        router.sendline(userPassword)
        router.expect('#')
        router.sendline('term len 0')
        router.expect('#')

        # Gather device name / port 

        router.logfile = tmpf
        router.sendline('show cdp neighbor | include SEP')
        router.expect('#')
        router.logfile = None

        rePrompt = re.compile(hostname)


        #output = {}

        # iterate through the tmpfile, build the output dict entries
        reEndLine = re.compile('\r\n')
        reShowCDP = re.compile(r'show cdp neighbor*')
        reCODEC = re.compile(r'CTS-CODEC')
        tmpf.seek(0)
        buf = tmpf.readlines()

        for line in buf:

            line = reEndLine.sub('', line)
            # Skip these conditions from further discovery:
            if re.match(reShowCDP,line): continue
            if re.match(rePrompt,line): continue
            if re.search(reCODEC,line): continue

            output[line.split()[0]] = {

                "switch":   hostname, 
                "name":     line.split()[0],
                "port":     line.split()[1] + " " + line.split()[2]
                }
            phonelist.append(line.split()[0])
        tmpf.close()
        
        i = 0
        for x in output:
            if output[x]['switch'] == hostname:
                i += 1
        numberphones = i
        
        print "Found " + str(numberphones) + " phones on " + hostname
    
        print "Grabbing phone details from switch " + hostname + ", and connecting to phones web interfaces for extended info"
        for phone in phonelist:
            tmpf2 = os.tmpfile()
            router.logfile = tmpf2
            router.sendline('show cdp neighbor '+output[phone]['port']+' detail')
            router.expect("#")
            router.logfile = None

            reEndLine2 = re.compile('\r\n')

            reShowCDP2 = re.compile(r'show cdp neighbor*')
            rePrompt2 = re.compile("^.*#")
            tmpf2.seek(0)
            buf = tmpf2.readlines()
            for line in buf:
                line = reEndLine2.sub('', line)
                if re.match(reShowCDP2,line): continue
                if re.match(rePrompt2,line): continue
                findIP = re.search('\s+IP address:\s+(\d+\.\d+\.\d+\.\d+)',line)
                findPlat = re.search('Platform:\s+(.+),',line)
                if findPlat:
                    output[phone]['platform'] = findPlat.group(1)
                if findIP:
                    output[phone]['ipaddy'] = findIP.group(1)
                    # connect to http://<ipaddy>/DeviceInformationX to grab xml
                    url = 'http://' + output[phone]['ipaddy'] + '/DeviceInformationX'
                    try:
                        x = urllib.urlopen(url)
                    except IOError:
                        output[phone]['serialnum'] = "UNKNOWN - can't connect to " + url
                        output[phone]['modelnum'] = "UNKNOWN - can't connect to " + url
                        output[phone]['MAC'] = "UNKNOWN - can't connect to " + url
                        continue

                    try:

                        xmltree = etree.parse(x)
                    except:
                        # General Error catchall
                        output[phone]['serialnum'] = "UNKNOWN - can't parse xml"
                        output[phone]['modelnum'] = "UNKNOWN - can't parse xml"
                        output[phone]['MAC'] = "UNKNOWN - can't parse xml"
                        continue
                    else:
                        for ele in xmltree.iter("serialNumber"):
                            output[phone]['serialnum'] = ele.text
                        for ele in xmltree.iter("MACAddress"):
                            output[phone]['MAC'] = ele.text
                        for ele in xmltree.iter("modelNumber"):
                            output[phone]['modelnum'] = ele.text
            tmpf2.close()
            
    # outfile header row
    outputfile.write("Switch,Port,PhoneName,IP,Platform(From CDP),SerialNumber,MACAddress,ModelNumber\n")  
    # Populate UNKNOWN where keys are missing. 
    for key in sorted(output.iterkeys()):
        if not output[key].has_key('serialnum'): output[key]['serialnum'] = "UNKNOWN"
        if not output[key].has_key('platform'): output[key]['platform'] = "UNKNOWN"
        if not output[key].has_key('ipaddy'): output[key]['ipaddy'] = "UNKNOWN"
        if not output[key].has_key('modelnum'): output[key]['modelnum'] = "UNKNOWN"
        if not output[key].has_key('MAC'): output[key]['MAC'] = "UNKNOWN"
        outputfile.write(output[key]['switch']+","+output[key]['port']+","+output[key]['name']+","+output[key]['ipaddy']+","+output[key]['platform']+","+output[key]['serialnum']+","+output[key]['MAC']+","+output[key]['modelnum']+"\n")

    print "script complete - results can be found in: "+opspath+'CIPphoneinfo_Output/'+ofile

    router.sendline ('exit')
    sys.exit(0)

if __name__ == "__main__":

    # We are running this module directly, rather than via import
    main()