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()