You are currently viewing Getting CUCM Real-Time Data via Risport70 with Python and Zeep (Cisco Serviceability API)

Getting CUCM Real-Time Data via Risport70 with Python and Zeep (Cisco Serviceability API)

Update 27.09.2022: The previous code stopped working in later releases of CUCM. This is because the WSDL definition changed and set the location to “localhost” instead of the actual node name. The updated code specifically defines a location and a SOAP Binding.

The Cisco Serviceability API consists of several API endpoints, however this post will focus on the RisPort70 API endpoint, which allows retrieval of the real-time status of devices including registration state, IP Address, model information and load information (Firmware version etc). The RisPort70 API replaces the deprecated RisPort API.

API Methods

Two of the main methods provided by this API endpoint are the SelectCmDevice and SelectCmDeviceExt. The main difference is that the SelectCmDevice can result in duplicated entries as it reports the results on a per-node basis. For example, if a telephone has temporarily registered to its backup call processing node and then gracefully switched back to its primary, the SelectCmDevice will likely show the device on both CmNodes (one as unregistered and the other as registered). The SelectCmDeviceExt method however collates the devices status records across nodes and ensures that there aren’t duplicate entries, so this is the one I’ll be using in this post.
Note: the SelectCmDeviceExt method does not accept a wildcard for items listed, so the exact device or directory number must be specified

A third method SelectCtiItem is also included which allows real-time CTI Application status (i.e. Application Name, authenticated user, IP Address, controlled CTI Devices and connection status). I won’t be covering this method in this post as I have yet to find a compelling use case in day to day operations work

Rate Limiting

Any single SelectCmDeviceExt query will return only a maximum of 1000 entries, and will not show an indication of whether there were actually only 1000 entries present or that the results were truncated. For this reason the API documentation recommends making queries using a list of specific devices, so that you can be certain you are getting all of the results. This could be done by using the AXL API to perform a listPhone request and storing the returned names in a list that is passed to the “SelectItems” parameter.

The API also has by default a rate limit of 15 queries per minute. This limit is defined via the Enterprise Parameter “Allowed Device Queries Per Minute”.

Since you can only receive a maximum of 1000 devices per query, if you have a large environment (more than 15000 devices) then you’ll need to also implement some logic to throttle your requests if you exceed this limit.

Querying the RisPort70 API with Python Zeep

Zeep is my new “go-to” library for consuming SOAP APIs. It’s well maintained, compatible with Python 3.x and much faster than SUDS.

Unlike with Cisco AXL, where we download the WDSL definitions to a local file first, with RisPort70 we’ll be referencing the WDSL file directly via the URL. Zeep will download the WDSL file and find the correct bindings and endpoint location, so these do not need to be specified as they do when using AXL.

Here is some boilerplate code I’ll be using to initiate a client object that will be used to issue the queries to the RisPort70 API.

# -*- coding: utf-8 -*-
 
from zeep import Client
from zeep.cache import SqliteCache
from zeep.transports import Transport
from zeep.exceptions import Fault
from zeep.plugins import HistoryPlugin
from requests import Session
from requests.auth import HTTPBasicAuth
from urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning
from lxml import etree
 
 
disable_warnings(InsecureRequestWarning)

# Cluster specific variables
username = 'admin'
password = 'password'
server = 'cucm.local'


wsdl = f'https://{server}:8443/realtimeservice2/services/RISService70?wsdl'

location = f'https://{server}:8443/realtimeservice2/services/RISService70'
binding = '{http://schemas.cisco.com/ast/soap}RisBinding' session = Session() session.verify = False session.auth = HTTPBasicAuth(username, password) transport = Transport(cache=SqliteCache(), session=session, timeout=20) history = HistoryPlugin() client = Client(wsdl=wsdl, transport=transport, plugins=[history]) service = client.create_service(binding, location) def show_history(): for hist in [history.last_sent, history.last_received]: print(etree.tostring(hist["envelope"], encoding="unicode", pretty_print=True))

Here we only need to specify the username, password and server (hostname or IP Address). I’m using the new Python “f-strings” formatting to build the WSDL URL using the hostname or IP Address stored in the server variable (see https://realpython.com/python-f-strings/ for more info). If you’re using an older version of Python then you should use the .format() variation instead.

I’ve also included an optional simple “show_history()” method that I often use during debugging to view the sent/received SOAP envelopes.

Next we need to build a CmSelectionCriteria object to define which devices we’re interested in and then execute the query using the SelectCmDeviceExt method. As mentioned earlier, you can’t use wildcards with SelectCmDeviceExt, so I’m specifying the exact phone names I’m interested in. In dCloud I only had Jabber Softphones to work with which in this Sandbox were named using the userid, thus no “SEPXXXXXXXXXXXX” Devices.

CmSelectionCriteria = {
    'MaxReturnedDevices': '10',
    'DeviceClass': 'Phone',
    'Model': '255',
    'Status': 'Any',
    'NodeName': '',
    'SelectBy': 'Name',
    'SelectItems': {
        'item': [
            'cholland',
            'amckenzie'
        ]
    },
    'Protocol': 'Any',
    'DownloadStatus': 'Any'
}

StateInfo = ''

try:
    resp = service.selectCmDeviceExt(CmSelectionCriteria=CmSelectionCriteria, StateInfo=StateInfo)
except Fault:
    show_history()

Here’s a summarised breakdown of the CmSelectionCriteria

  • MaxReturnedDevices – Self explanatory. The maximum value you can enter is 1000.
  • DeviceClass – “Phone” or “Any” are generally the only classes I’ve queried against, but there’s also Gateway, H323, MediaResources, VoiceMail, HuntList, SIPTrunk, Unknown and Cti
  • Model – This is a numeric value that is stored in the Database records on CUCM, not the human readable model. You can use ‘255’ to list any/all models or refer to the API Reference which lists all of the Model Description to numeric mappings here: https://developer.cisco.com/docs/sxml/#!risport70-api-reference/selectcmdevice
  • NodeName – Restricts the query to a specific CUCM node. Pass a blank string to query all nodes.
  • SelectBy – I usually use Name, but the API Documentation also lists IPV4Address, IPV6Address, DirNumber, Description or SIPStatus
  • SelectItem – Contains a list of <item> tags. Here you can specify the device name (i.e. SEPXXXXXXXXXXXX / CSFXXXXX) or directory number, depending on what you’ve used in “SelectBy”. If you’re using SelectCmDevice instead of SelectCmDeviceExt you can also use a wildcard with *, however be sure your query is specific enough to not return more than 1000 results as mentioned earlier in the rate limiting section.
  • Protocol – SIP, SCCP, Any or Unknown
  • DownloadStatus – Any, Upgrading, Successful, Failed or Unknown are possible choices.

The final parameter passed in the actual request is the StateInfo. For your first query you’ll always pass in a blank value for this. The response object returned will then include a new StateInfo represented as a string that you can use in subsequent requests. If there have been no changes at all since your last query, then RisPort70 will return a response with “TotalDevicesFound = 0” and “NoChange = true” (per node). If however any device has changed state since the last query then you’ll receive the complete results for the node that has a “NoChange = false”. I had hoped the StateInfo would be a bit more functional and only show the devices that have changed state since the last request but alas that is not the case.

Below is an example of an initial response:

{
    'SelectCmDeviceResult': {
        'TotalDevicesFound': 2,
        'CmNodes': {
            'item': [
                {
                    'ReturnCode': 'NotFound',
                    'Name': 'imp-pub.dcloud.cisco.com',
                    'NoChange': True,
                    'CmDevices': {
                        'item': []
                    }
                },
                {
                    'ReturnCode': 'Ok',
                    'Name': 'ucm-pub.dcloud.cisco.com',
                    'NoChange': False,
                    'CmDevices': {
                        'item': []
                    }
                },
                {
                    'ReturnCode': 'Ok',
                    'Name': 'ucm-sub1.dcloud.cisco.com',
                    'NoChange': False,
                    'CmDevices': {
                        'item': [
                            {
                                'Name': 'cholland',
                                'DirNumber': '+19725555018-Registered',
                                'DeviceClass': 'Phone',
                                'Model': 503,
                                'Product': 390,
                                'BoxProduct': 0,
                                'Httpd': 'No',
                                'RegistrationAttempts': 4,
                                'IsCtiControllable': True,
                                'LoginUserId': 'cholland',
                                'Status': 'Registered',
                                'StatusReason': 0,
                                'PerfMonObject': 2,
                                'DChannel': 0,
                                'Description': 'Charles Holland cholland +19725555018',
                                'H323Trunk': {
                                    'ConfigName': None,
                                    'TechPrefix': None,
                                    'Zone': None,
                                    'RemoteCmServer1': None,
                                    'RemoteCmServer2': None,
                                    'RemoteCmServer3': None,
                                    'AltGkList': None,
                                    'ActiveGk': None,
                                    'CallSignalAddr': None,
                                    'RasAddr': None
                                },
                                'TimeStamp': 1546078613,
                                'Protocol': 'SIP',
                                'NumOfLines': 1,
                                'LinesStatus': {
                                    'item': [
                                        {
                                            'DirectoryNumber': '+19725555018',
                                            'Status': 'Registered'
                                        }
                                    ]
                                },
                                'ActiveLoadID': 'Jabber_for_Windows-11.8.3.51659',
                                'InactiveLoadID': 'Jabber_for_Windows-11.8.3.51659',
                                'DownloadStatus': 'Unknown',
                                'DownloadFailureReason': None,
                                'DownloadServer': None,
                                'IPAddress': {
                                    'item': [
                                        {
                                            'IP': '198.18.133.37',
                                            'IPAddrType': 'ipv4',
                                            'Attribute': 'Unknown'
                                        }
                                    ]
                                }
                            },
                            {
                                'Name': 'amckenzie',
                                'DirNumber': '+19725555016-Registered',
                                'DeviceClass': 'Phone',
                                'Model': 503,
                                'Product': 390,
                                'BoxProduct': 0,
                                'Httpd': 'No',
                                'RegistrationAttempts': 1,
                                'IsCtiControllable': True,
                                'LoginUserId': 'amckenzie',
                                'Status': 'Registered',
                                'StatusReason': 0,
                                'PerfMonObject': 2,
                                'DChannel': 0,
                                'Description': 'Adam McKenzie amckenzie +19725555016',
                                'H323Trunk': {
                                    'ConfigName': None,
                                    'TechPrefix': None,
                                    'Zone': None,
                                    'RemoteCmServer1': None,
                                    'RemoteCmServer2': None,
                                    'RemoteCmServer3': None,
                                    'AltGkList': None,
                                    'ActiveGk': None,
                                    'CallSignalAddr': None,
                                    'RasAddr': None
                                },
                                'TimeStamp': 1546076908,
                                'Protocol': 'SIP',
                                'NumOfLines': 1,
                                'LinesStatus': {
                                    'item': [
                                        {
                                            'DirectoryNumber': '+19725555016',
                                            'Status': 'Registered'
                                        }
                                    ]
                                },
                                'ActiveLoadID': 'Jabber_for_Windows-11.8.3.51659',
                                'InactiveLoadID': 'Jabber_for_Windows-11.8.3.51659',
                                'DownloadStatus': 'Unknown',
                                'DownloadFailureReason': None,
                                'DownloadServer': None,
                                'IPAddress': {
                                    'item': [
                                        {
                                            'IP': '198.18.133.36',
                                            'IPAddrType': 'ipv4',
                                            'Attribute': 'Unknown'
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                }
            ]
        }
    },
    'StateInfo': '<stateinfo><node name="imp-pub.dcloud.cisco.com" subsystemstarttime="0" stateid="0" totalitemsfound="0" totalitemsreturned="0"><node name="ucm-pub.dcloud.cisco.com" subsystemstarttime="1546075700" stateid="2" totalitemsfound="1" totalitemsreturned="1"><node name="ucm-sub1.dcloud.cisco.com" subsystemstarttime="1546075022" stateid="8" totalitemsfound="2" totalitemsreturned="2"></node></node></node>'
}

I can use the returned StateInfo in a subsequent query to get results only for cluster nodes that have devices that changed status. In this example no nodes had any devices that changed state, so the “TotalDevicesFound” is 0.

PrevStateInfo = resp.StateInfo

try:
    resp = service.selectCmDeviceExt(CmSelectionCriteria=CmSelectionCriteria, StateInfo=PrevStateInfo)
except Fault:
    show_history()
    
print(resp)

{
    'SelectCmDeviceResult': {
        'TotalDevicesFound': 0,
        'CmNodes': {
            'item': [
                {
                    'ReturnCode': 'NotFound',
                    'Name': 'imp-pub.dcloud.cisco.com',
                    'NoChange': True,
                    'CmDevices': {
                        'item': []
                    }
                },
                {
                    'ReturnCode': 'Ok',
                    'Name': 'ucm-pub.dcloud.cisco.com',
                    'NoChange': True,
                    'CmDevices': {
                        'item': []
                    }
                },
                {
                    'ReturnCode': 'Ok',
                    'Name': 'ucm-sub1.dcloud.cisco.com',
                    'NoChange': True,
                    'CmDevices': {
                        'item': []
                    }
                }
            ]
        }
    },
    'StateInfo': '<StateInfo><Node Name="imp-pub.dcloud.cisco.com" SubsystemStartTime="0" StateId="0" TotalItemsFound="0" TotalItemsReturned="0"/><Node Name="ucm-pub.dcloud.cisco.com" SubsystemStartTime="1546075700" StateId="2" TotalItemsFound="1" TotalItemsReturned="1"/><Node Name="ucm-sub1.dcloud.cisco.com" SubsystemStartTime="1546075022" StateId="8" TotalItemsFound="2" TotalItemsReturned="2"/></StateInfo>'
}

A Practical Example

I also wanted to include a practical example for something that I would find useful in a production environment, such as doing a before/after scan after implementing some cluster changes like a CUCM upgrade or CTI File update to identify which specific devices may have not re-registered. This script will perform the following:

  • Use the AXL API to get a list of all phones
  • Using this list, perform a SelectCmDeviceExt query
  • Store the results in a to be used as a comparison at a later time
  • Prompt the user to reissue the query by pressing “Enter”. Every new query is compared against the initial capture when first running the script.

The script is limited to clusters with less than 1000 phones, as I didn’t include logic to account for this. This would be quite simple to add in by checking the length of the listPhone request and using list splicing to split requests into 1000 unit chunks. It also doesn’t include any persistent storage of the “snapshot”, so if you exit the script you won’t be able to compare later.

You can find the complete script on my github repo here: https://github.com/ptursan/cucm-compare-reg-status/

Disclaimer: The script is provided as is and is just an example to be used at your own risk (i.e. if you overburden your server by hammering Enter thousands of times). I take no responsibility for any misuse or adverse effects.