CUCM Monitoring (SXML)
via Provisioning Portal API

In this section, you will implement the following CUCM Serviceability XML API tasks in your Provisioning Portal API:

  1. Search for devices in the Real-Time Information Database
  2. Get CUCM services status information
  3. Get performance monitoring counters from CUCM
  4. Verification

Step 1 - Device Search

Earlier you used Python to retrieve information from the Serviceability XML API. You will now incorporate similar functionality into the portal to retrieve the phone device registration details from your CUCM Real-Time Information Database. You will achieve this by leveraging the global CUCM SOAP API Python class mySXMLRisPort70Service that you instantiated earlier.

With the help of your CUCM SOAP API Python class mySXMLRisPort70Service you will send a selectCMDevice RisPort70 API request and relay the response if successful.

All of the changes again will be implemented in flaskr/api/v1/cucm.py
  1. In your VS Code tab, using the Explorer function, open up flaskr/api/v1/cucm.py
  2. In the cucm_device_search_api() class, def get(self): method, locate the existing line raise Exception('Not implemented yet!!') remove it and replace it with the following highlighted lines.

    @api.route("/device_search")
    class cucm_device_search_api(Resource):
        @api.expect(cucm_device_search_criteria_query_args, validate=True)
        def get(self):
            """
            Perform a Device Search via RisPort70 (Real-Time Information Port) service on CUCM given the search criteria
    
            This API method executes a SelectCMDevice Request and sets results with returned Response data
    
            https://developer.cisco.com/docs/sxml/risport70-api/#selectcmdevice
    
            """
            try:
                cucm_device_search_criteria_query_parsed_args = cucm_device_search_criteria_query_args.parse_args(request)
                SearchItems = []
                if ',' in cucm_device_search_criteria_query_parsed_args['SearchItems']:
                    SearchItems_str = cucm_device_search_criteria_query_parsed_args['SearchItems']
                    SearchItems = list(map(str.strip, SearchItems_str.split(',')))
                else:
                    SearchItems.append(cucm_device_search_criteria_query_parsed_args['SearchItems'])
                ris_search_criteria = {
                    'SelectBy': cucm_device_search_criteria_query_parsed_args['SearchBy'],
                    'MaxReturnedDevices': 1000,
                    'Status': cucm_device_search_criteria_query_parsed_args['Status'],
                    'SelectItems': []
                }
                for SearchItem in SearchItems:
                    SelectItem_dict = {'item': SearchItem}
                    ris_search_criteria['SelectItems'].append(SelectItem_dict)
                risresult = mySXMLRisPort70Service.ris_query(search_criteria=ris_search_criteria)
            except Exception as e:
                apiresult = {'success': False, 'message': str(e)}
                return jsonify(apiresult)
            apiresult = {'success': True, 'message': "Device Search Results Retrieved Successfully",
                            'TotalDevicesFound': risresult['SelectCmDeviceResult']['TotalDevicesFound'],
                            'ris_search_result': risresult}
            return jsonify(apiresult)

    The first part of this code is just parsing through the input parameters and then creating the data structure that is required for the selectCMDevice API call. Once the data is in the right format, the ris_query method is called. This API call queries CUCM for the real-time information of the devices that match the search criteria and returns a dictionary with the results. The data is extracted from the result and returned as part of the apiresult.

  3. Save this file by going to Application Menu → File → Save or by simply Pressing Ctrl+S

Step 2 - CUCM Services Status

Next let's work on the code to get CUCM Services Status using the Portal API. You will achieve this by leveraging the global CUCM SOAP API Python class mySXMLControlCenterServicesService that you instantiated earlier.

With the help of your CUCM SOAP API Python class mySXMLControlCenterServicesService you will send a soapGetServiceStatus Control Center Services API request and relay the response if successful.

All of the changes again will be implemented in flaskr/api/v1/cucm.py
  1. In your VS Code tab, using the Explorer function, open up flaskr/api/v1/cucm.py
  2. In the cucm_service_api() class, def get(self): method, locate the existing line raise Exception('Not implemented yet!!') remove it and replace it with the following highlighted lines.

    @api.route("/service")
    class cucm_service_api(Resource):
        @api.expect(cucm_service_status_query_args, validate=True)
        def get(self):
            """
            Perform a Service Status Query via ControlCenterServicesPort service on CUCM given the service_name
    
            This API method executes a soapGetServiceStatus Request and sets results with returned Response data
    
            https://developer.cisco.com/docs/sxml/control-center-services-api/#soapgetservicestatus
    
            """
            try:
                cucm_service_status_query_parsed_args = cucm_service_status_query_args.parse_args(request)
                service_list = None
                if cucm_service_status_query_parsed_args['Services'] is not None:
                    services_str = cucm_service_status_query_parsed_args['Services']
                    service_list = list(map(str.strip, services_str.split(',')))
                ccsresult = mySXMLControlCenterServicesService.ccs_get_service_status(service_list=service_list)
            except Exception as e:
                apiresult = {'success': False, 'message': str(e)}
                return jsonify(apiresult)
            apiresult = {'success': True, 'message': "Service(s) Status Info Retrieved Successfully",
                            'service_info': ccsresult}
            return jsonify(apiresult)

    Like the previous example, this code is just parsing through the input parameters and then creating the data structure that is required for the soapGetServiceStatus API call. Once the data is in the right format, the ccs_get_service_status method is called. This API call queries CUCM for the status of the services that match the search criteria and returns a dictionary with the results. The data is extracted from the result and returned as part of the apiresult.

  3. Save this file by going to Application Menu → File → Save or by simply Pressing Ctrl+S

Step 3 - CUCM Performance Monitoring Counters

Next you will implement the ability to retrieve CUCM performance counters in the Portal API. You will achieve this by leveraging the global CUCM SOAP API Python class mySXMLControlCenterServicesService that you instantiated earlier.

This Portal API will execute the following logic:

  1. If the perfmon_counters parameter is supplied, then do the following:
    1. Open a performance monitoring session
    2. Add the PerfMon counters that you would like to poll to the monitoring session
    3. Collect PerfMon session data to retrieve the counter values
    4. If any of the PerfMon counter names include a "%" or "Percentage", pause for 5 seconds and collect PerfMon session data again in order to get accurate sampling.
    5. Close the Performance Monitoring Session
  2. If perfmon_class parameter is supplied, collect the entire class of counters via the perfmonCollectCounterData API method

With the help of the CUCM SOAP API Python class mySXMLPerfMonService and the input parameters supplied you will execute the following CUCM PerfMon API methods in order to efficiently retrieve the performance monitoring counters' values.

and relay a final response if all PerfMon API requests are successful.

All of the changes again will be implemented in flaskr/api/v1/cucm.py
  1. In your VS Code tab, using the Explorer function, open up flaskr/api/v1/cucm.py
  2. In the cucm_perfmon_api() class, def post(self): method, locate the existing line raise Exception('Not implemented yet!!') remove it and replace it with the following highlighted lines.

    @api.route("/perfmon")
    class cucm_perfmon_api(Resource):
        # CUCM Perfmon Query API Payload Model
        cucm_perfmon_post_data = api.model('perfmon_post_data', {
            "perfmon_class": fields.String(description='Performance Class Name', example='Cisco SIP Stack', required=False),
            "perfmon_counters": fields.List(fields.String(description='Performance Counter Class + Instance + Name',
                                                            example='Cisco CallManager\\RegisteredOtherStationDevices', required=False))
        })
    
        @api.expect(cucm_perfmon_post_data, validate=True)
        def post(self, perfmon_class=None, perfmon_counters=None):
            """
            Query Performance Counters via PerfMon service on CUCM
    
            This API Method needs to be a POST method even though we are not creating any new items/resources because Swagger UI does
            not allow a payload body for GET requests.
    
            This API method executes multiple PerfMon API requests to get the Performance Counters values and sets results with returned Response data
    
            https://developer.cisco.com/docs/sxml/perfmon-api/
    
            """
            try:
                # If no perfmon_class and perfmon_counters are passed via api.payload and via post method arguments, we can't proceed
                if not ('perfmon_class' in api.payload or 'perfmon_counters' in api.payload) and not (perfmon_class or perfmon_counters):
                    raise Exception(f"perfmon_class or perfmon_counters is required in the payload or post method arguments")
    
                # If perfmon_class is NOT defined via post method argument but its in the api.payload then use it
                if not perfmon_class and api.payload.get('perfmon_class'):
                    perfmon_class = api.payload.get('perfmon_class')
                if perfmon_class:
                    perfmon_class_result = mySXMLPerfMonService.perfmon_query_class(perfmon_class_name=perfmon_class)
                # If perfmon_class is still not defined then set result to None
                else:
                    perfmon_class_result = None
    
                # If perfmon_counters is NOT defined via post method argument but its in the api.payload then use it
                if not perfmon_counters and api.payload.get('perfmon_counters'):
                    perfmon_counters = api.payload.get('perfmon_counters')
                if perfmon_counters:
                    perfmon_session_handle = mySXMLPerfMonService.perfmon_open_session()
                    perfmon_counter_list = []
                    for counter in perfmon_counters:
                        perfmon_counter_list.append(f"\\\\{cucm_host}\\" + counter)
                    if not mySXMLPerfMonService.perfmon_add_counter(session_handle=perfmon_session_handle, counters=perfmon_counter_list):
                        mySXMLPerfMonService.perfmon_close_session(session_handle=perfmon_session_handle)
                        raise Exception(f"Failed to Query Counters: {api.payload['perfmon_counters']}")
                    perfmon_counters_result = mySXMLPerfMonService.perfmon_collect_session_data(session_handle=perfmon_session_handle)
                    if any(("%" in counter) or ("Percentage" in counter) for counter in perfmon_counters):
                        sleep(5)
                        perfmon_counters_result = mySXMLPerfMonService.perfmon_collect_session_data(session_handle=perfmon_session_handle)
                    mySXMLPerfMonService.perfmon_close_session(session_handle=perfmon_session_handle)
                # If perfmon_class is still not defined then set result to None
                else:
                    perfmon_counters_result = None
            except Exception as e:
                apiresult = {'success': False, 'message': str(e)}
                return jsonify(apiresult)
            apiresult = {'success': True, 'message': "PerfMon Data Retrieved Successfully",
                            'perfmon_class_result': perfmon_class_result,
                            'perfmon_counters_result': perfmon_counters_result}
            return jsonify(apiresult)

    Again, this is quite a bit of code but it is just parsing through the input parameters and depending on what is passed, it will construct the list of performance counters in the format that is required for the perfmonAddCounter API call. The counters are then passed to the perfmon_collect_session_data method which queries CUCM for the performance counter values and returns a dictionary with the results. According to the documentation, some counters do not return a result on the first query and require some time for the data to be populated. As a result, if the counter name includes a "%" or "Percentage", the code will pause for 5 seconds and collect the data again. The data is extracted from the result and returned as part of the apiresult after closing the session.

    If you were writing an application that needed to poll these counters on a regular basis, you would not close the session after each query. Instead, you would open the session once and then add the counters and collect the data as needed. You would only close the session when you were done polling the counters.

  3. Save this file by going to Application Menu → File → Save or by simply Pressing Ctrl+S

Step 4 - Verification

You have added even more capabilities to your Provisioning Portal's APIs, now it is time to make sure it all works as expected, again utilizing your Portal's RESTPlus/Swagger UI page.

  1. In VS Code, make sure flaskr/api/v1/cucm.py file is saved
  2. Restart your Flask app by clicking the green restart button from the controller: which you should always see along the top of the window, when Flask is running.
    Or, if Flask is not started at all, click the Debug button on the left side followed by the green start/play button next to Start LTRCOL-2574 Portal
  3. If the Terminal window at bottom right shows: * Running on http://10.0.108.40.40:5000/ (Press CTRL+C to quit), then your Flask app is running.
  4. Access the RESTPlus/Swagger UI page at http://dev1.pod8.col.lab:5000/api/v1/
  5. Expand the cucm section.
  6. Perform the following individual checks:
    1. Search for Devices in the Real-Time Information Database
      1. Click the GET /cucm/device_search operation.
      2. On the right, click Try it out.
      3. For the SearchItems Parameter, enter CSFPOD8UCMUSER
      4. In the expanded section, click Execute.
      5. Scroll down and examine the server response. You should note a server response code of 200 and a response body with the details of the Phone Device Configuration. Here is a sample response body:
            {
                "TotalDevicesFound": 1,
                "message": "Device Search Results Retrieved Successfully",
                "ris_search_result": {
                    "SelectCmDeviceResult": {
                    "CmNodes": {
                        "item": [
                        {
                            "CmDevices": {
                            "item": [
                                {
                                "ActiveLoadID": "Webex_for_Windows-45.5.0.32411",
                                ...
                                "Description": "Cisco Live LTRCOL-2574 - pod8ucmuser",
                                "DeviceClass": "Phone",
                                "DirNumber": "\+19197080001-UnRegistered",
                                "DownloadFailureReason": null,
                                "DownloadServer": null,
                                "DownloadStatus": "Unknown",
                                ...
                                "Httpd": "No",
                                "IPAddress": {
                                    "item": [
                                    {
                                        "Attribute": "Unknown",
                                        "IP": "10.0.108.45",
                                        "IPAddrType": "ipv4"
                                    }
                                    ]
                                },
                                "InactiveLoadID": "Webex_for_Windows-45.5.0.32411",
                                "IsCtiControllable": true,
                                "LinesStatus": {
                                    "item": [
                                    {
                                        "DirectoryNumber": "\+19197080001",
                                        "Status": "UnRegistered"
                                    }
                                    ]
                                },
                                "LoginUserId": "pod8ucmuser",
                                "Model": 503,
                                "Name": "CSFPOD8UCMUSER",
                                "NumOfLines": 1,
                                "PerfMonObject": 2,
                                "Product": 390,
                                "Protocol": "SIP",
                                "RegistrationAttempts": 0,
                                "Status": "UnRegistered",
                                "StatusReason": 0,
                                "TimeStamp": 1654799252
                                }
                            ]
                            },
                            "Name": "cucm1a.pod8.col.lab",
                            "NoChange": false,
                            "ReturnCode": "Ok"
                        }
                        ]
                    },
                    "TotalDevicesFound": 1
                    },
                    "StateInfo": ""
                },
                "success": true
            }

        If you recall, you deleted this CSF device in the last section, so you might be wondering why you are still getting a result back from this API call. The Real-time Information System (RIS) database is an in-memory store and does not pay attention to what is in the CUCM database. RIS will maintain information about unregistered devices for a period of time, even if the device has been deleted from the database. The in-memory store cleanup is controlled via CUCM RIS Data Collector Service Parameter named RIS Unused Cisco CallManager Device Store Period which defaults to 3 days and could be as high as 30 days.

    2. Get CUCM Services Status Information
      1. Click the GET /cucm/service operation.
      2. On the right, click Try it out.
      3. For the Services Parameter, enter Cisco CallManager, Cisco Tftp
      4. In the expanded section, click Execute.
      5. Scroll down and examine the server response. You should note a server response code of 200 and a response body with the uuids of the newly added phone and the associated user you just updated. Here is a sample response body:
            {
                "message": "Service(s) Status Info Retrieved Successfully",
                "service_info": {
                    "ReasonCode": -1,
                    "ReasonString": null,
                    "ReturnCode": {
                    "_value_1": "0"
                    },
                    "ServiceInfoList": {
                    "item": [
                        {
                        "ReasonCode": -1,
                        "ReasonCodeString": " ",
                        "ServiceName": "Cisco CallManager",
                        "ServiceStatus": "Started",
                        "StartTime": "Wed Feb  5 09:15:51 2025",
                        "UpTime": 1108052
                        },
                        {
                        "ReasonCode": -1,
                        "ReasonCodeString": " ",
                        "ServiceName": "Cisco Tftp",
                        "ServiceStatus": "Started",
                        "StartTime": "Wed Feb  5 09:16:51 2025",
                        "UpTime": 1108044
                        }
                    ]
                    }
                },
                "success": true
            }
                                
    3. Get Performance Monitoring Counters from CUCM
      1. Click the POST /cucm/perfmon operation.
      2. On the right, click Try it out.
      3. For the payload Parameter, enter the following payload.
        {
            "perfmon_class": "System",
            "perfmon_counters": [
                "Cisco CallManager\\RegisteredOtherStationDevices",
                "Cisco Tomcat Connector(http-bio-8443)\\ThreadsBusy"
            ]
        }
      4. In the expanded section, click Execute.
      5. Scroll down and examine the server response. You should note a server response code of 200 and a response body with the uuid phone device you just updated Here is a sample response body:
            {
                "message": "PerfMon Data Retrieved Successfully",
                "perfmon_class_result": [
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\Allocated FDs"
                    },
                    "Value": 7712
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\Being Used FDs"
                    },
                    "Value": 7712
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\Freed FDs"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOAwait"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOCpuUtil"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOKBytesReadPerSecond"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOKBytesWrittenPerSecond"
                    },
                    "Value": 229
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOPerSecond"
                    },
                    "Value": 24
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOReadReqMergedPerSecond"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOReadReqPerSecond"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOReqQueueSizeAvg"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOSectorsReadPerSecond"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOSectorsReqSizeAvg"
                    },
                    "Value": 18
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOSectorsWrittenPerSecond"
                    },
                    "Value": 458
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOServiceTime"
                    },
                    "Value": 0
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOWriteReqMergedPerSecond"
                    },
                    "Value": 8
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\IOWriteReqPerSecond"
                    },
                    "Value": 24
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\Max FDs"
                    },
                    "Value": 582286
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\Total CPU Time"
                    },
                    "Value": 109266374
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\Total Processes"
                    },
                    "Value": 207
                    },
                    {
                    "CStatus": 1,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\System\\Total Threads"
                    },
                    "Value": 1138
                    }
                ],
                "perfmon_counters_result": [
                    {
                    "CStatus": 0,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\Cisco CallManager\\RegisteredOtherStationDevices"
                    },
                    "Value": 2
                    },
                    {
                    "CStatus": 0,
                    "Name": {
                        "_value_1": "\\\\cucm1a.pod8.col.lab\\Cisco Tomcat Connector(http-bio-8443)\\ThreadsBusy"
                    },
                    "Value": 1
                    }
                ],
                "success": true
            }

        Notice that the query included the System class and two counters. The response includes the values of the all the counters in the System class and the two counters that were queried.

You have now added some core CUCM monitoring capabilities to your Portal APIs that allows you to keep an eye on the health and operational state of your CUCM servers. Let's add some core REST API capabilities to your Portal which will help you with Webex APIs.


import re
from time import sleep
from flask import jsonify
from flask import request
from flask import Blueprint
from flask_restx import Namespace, Resource, fields, reqparse
from flaskr.cucm.v1.cucm import AXL, PAWS, SXML
from flaskr.api.v1.parsers import cucm_add_phone_query_args
from flaskr.api.v1.parsers import cucm_update_phone_query_args
from flaskr.api.v1.parsers import cucm_list_phones_returned_tags_query_args
from flaskr.api.v1.parsers import cucm_list_phones_search_criteria_query_args
from flaskr.api.v1.parsers import cucm_device_search_criteria_query_args
from flaskr.api.v1.parsers import cucm_service_status_query_args
from flaskr.api.v1.parsers import cucm_update_line_query_args
from flaskr.api.v1.parsers import cucm_update_user_query_args
from os import getenv

api = Namespace('cucm', description='Cisco Unified Communications Manager APIs')

# Read environment variables
cucm_host = getenv('CUCM_HOSTNAME')
cucm_user = getenv('CUCM_USERNAME')
cucm_pass = getenv('CUCM_PASSWORD')

# Core CUCM SOAP API Python Class Instances
myAXL = AXL(cucm_host, cucm_user, cucm_pass)
myPAWSVersionService = PAWS(cucm_host, cucm_user, cucm_pass, 'VersionService')
mySXMLRisPort70Service = SXML(cucm_host, cucm_user, cucm_pass, 'realtimeservice2')
mySXMLControlCenterServicesService = SXML(cucm_host, cucm_user, cucm_pass, 'controlcenterservice2')
mySXMLPerfMonService = SXML(cucm_host, cucm_user, cucm_pass, 'perfmonservice2')

###########################################


@api.route("/version")
class cucm_get_version_api(Resource):
    def get(self):
        """
        Returns CUCM Active Software version

        This API method utilizes getActiveVersion PAWS requests along with the supplied parameters
        <br>
        https://developer.cisco.com/site/paws/documents/api-reference/
        """
        try:
            version_info = myPAWSVersionService.get_version()
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "CUCM Active Version Retrieved Successfully", 'version': version_info['version']}
        return jsonify(apiresult)


@api.route("/phone/<string:device_name>")
@api.param('device_name', description='The Name of the Phone Device')
class cucm_phone_api(Resource):
    def get(self, device_name):
        """
        Retrieves a Phone device configuration from CUCM

        This API method executes an getPhone AXL Request with the supplied device_name
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_getPhone.html
        """
        try:
            axlresult = myAXL.get_phone(device_name)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "Phone Data Retrieved Successfully", 'phone_data': axlresult['return']['phone']}
        return jsonify(apiresult)

    @api.expect(cucm_add_phone_query_args, validate=True)
    def post(self, device_name):
        """
        Adds a new Phone Device to CUCM

        This API method utilizes AXL getUser, addPhone and updateUser requests along with the supplied parameters
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_getUser.html
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_addPhone.html
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_updateUser.html
        <br>
        """
        try:
            cucm_add_phone_query_parsed_args = cucm_add_phone_query_args.parse_args(request)
            axl_get_user_result = myAXL.get_user(userid=cucm_add_phone_query_parsed_args['ownerUserName'])
            user_telephoneNumber = None
            if axl_get_user_result['return']['user'].get('telephoneNumber'):
                user_telephoneNumber = re.sub(r"^\+", "\\+", axl_get_user_result['return']['user']['telephoneNumber'])
            user_associatedDevices = []
            if axl_get_user_result['return']['user']['associatedDevices']:
                user_associatedDevices = axl_get_user_result['return']['user']['associatedDevices']['device']

            cucm_add_phone_data_dict = {
                "name": device_name,
                "description": cucm_add_phone_query_parsed_args['description'],
                "product": cucm_add_phone_query_parsed_args['phonetype'],
                "class": "Phone",
                "protocol": "SIP",
                "protocolSide": "User",
                "commonPhoneConfigName": "Standard Common Phone Profile",
                "callingSearchSpaceName": cucm_add_phone_query_parsed_args['devicecss'],
                "devicePoolName": "Default",
                "locationName": "Hub_None",
                "securityProfileName": "Universal Device Template - Model-independent Security Profile",
                "sipProfileName": "Standard SIP Profile",
                "ownerUserName": cucm_add_phone_query_parsed_args['ownerUserName'],
                "lines": {
                    "line": {
                        "index": "1",
                        "dirn": {
                            "pattern": user_telephoneNumber,
                            "routePartitionName": 'DN_PT'
                        },
                        "display": cucm_add_phone_query_parsed_args['calleridname'],
                        "displayAscii": cucm_add_phone_query_parsed_args['calleridname'],
                        "associatedEndusers": {
                                "enduser": {
                                    "userId": cucm_add_phone_query_parsed_args['ownerUserName']
                                }
                        }
                    }
                }
            }

            axl_add_phone_result = myAXL.add_phone(phone_data=cucm_add_phone_data_dict)
            user_associatedDevices.append(device_name)
            cucm_update_user_data_dict = {
                "userid": cucm_add_phone_query_parsed_args['ownerUserName'],
                'primaryExtension': {
                    "pattern": user_telephoneNumber,
                    "routePartitionName": 'DN_PT'
                },
                "associatedDevices": {
                    'device': user_associatedDevices
                }
            }
            axl_update_user_result = myAXL.update_user(user_data=cucm_update_user_data_dict)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "Phone Added Successfully",
                        'phone_uuid': axl_add_phone_result['return'],
                        'user_uuid': axl_update_user_result['return']}
        return jsonify(apiresult)

    @api.expect(cucm_update_phone_query_args, validate=True)
    def put(self, device_name):
        """
        Updates a Phone Device configuration and Applies it on CUCM

        This API method executes an updatePhone and applyPhone AXL Requests with the supplied phone_update_data parameter
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_updatePhone.html
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_applyPhone.html
        """
        try:
            cucm_update_phone_query_parsed_args = cucm_update_phone_query_args.parse_args(request)
            phone_update_data = {
                "name": device_name,
                "description": cucm_update_phone_query_parsed_args["description"],
                "isActive": cucm_update_phone_query_parsed_args["isActive"],
                "callingSearchSpaceName": cucm_update_phone_query_parsed_args["callingSearchSpaceName"]
            }
            axl_update_phone_result = myAXL.update_phone(phone_data=phone_update_data)
            myAXL.apply_phone(device_name)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "Phone Configuration Updated & Applied Successfully",
                        'uuid': axl_update_phone_result['return']}
        return jsonify(apiresult)

    def delete(self, device_name):
        """
        Deletes a Phone device from CUCM

        This API method executes an removePhone AXL Request with the supplied device_name
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_removePhone.html
        """
        try:
            axlresult = myAXL.delete_phone(device_name)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "Phone Successfully Deleted", 'uuid': axlresult['return']}
        return jsonify(apiresult)


@api.route("/user/<string:userid>")
@api.param('userid', description='CUCM End User ID')
class cucm_user_api(Resource):
    def get(self, userid):
        """
        Retrieves an End User's configuration from CUCM

        This API method executes an getUser AXL Request with the supplied userid
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_getUser.html
        """
        try:
            axlresult = myAXL.get_user(userid)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "User Data Retrieved Successfully", 'user_data': axlresult['return']['user']}
        return jsonify(apiresult)


    @api.expect(cucm_update_user_query_args, validate=True)
    def put(self, userid):
        """
        Updates an End User's Home Cluster configuration on CUCM

        This API method executes an updateUser AXL Request with the supplied userid
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_updateUser.html
        """
        try:
            cucm_update_user_query_args_parsed_args = cucm_update_user_query_args.parse_args(request)
            user_data = {
                'userid': userid,
                'homeCluster': cucm_update_user_query_args_parsed_args['homecluster']
            }
            axlresult = myAXL.update_user(user_data=user_data)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "User Updated Successfully", 'user_data': axlresult['return']}
        return jsonify(apiresult)


@api.route("/device_search")
class cucm_device_search_api(Resource):
    @api.expect(cucm_device_search_criteria_query_args, validate=True)
    def get(self):
        """
        Perform a Device Search via RisPort70 (Real-Time Information Port) service on CUCM given the search criteria

        This API method executes a SelectCMDevice Request and sets results with returned Response data

        https://developer.cisco.com/docs/sxml/risport70-api/#selectcmdevice

        """
        try:
            cucm_device_search_criteria_query_parsed_args = cucm_device_search_criteria_query_args.parse_args(request)
            SearchItems = []
            if ',' in cucm_device_search_criteria_query_parsed_args['SearchItems']:
                SearchItems_str = cucm_device_search_criteria_query_parsed_args['SearchItems']
                SearchItems = list(map(str.strip, SearchItems_str.split(',')))
            else:
                SearchItems.append(cucm_device_search_criteria_query_parsed_args['SearchItems'])
            ris_search_criteria = {
                'SelectBy': cucm_device_search_criteria_query_parsed_args['SearchBy'],
                'MaxReturnedDevices': 1000,
                'Status': cucm_device_search_criteria_query_parsed_args['Status'],
                'SelectItems': []
            }
            for SearchItem in SearchItems:
                SelectItem_dict = {'item': SearchItem}
                ris_search_criteria['SelectItems'].append(SelectItem_dict)
            risresult = mySXMLRisPort70Service.ris_query(search_criteria=ris_search_criteria)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "Device Search Results Retrieved Successfully",
                        'TotalDevicesFound': risresult['SelectCmDeviceResult']['TotalDevicesFound'],
                        'ris_search_result': risresult}
        return jsonify(apiresult)


@api.route("/service")
class cucm_service_api(Resource):
    @api.expect(cucm_service_status_query_args, validate=True)
    def get(self):
        """
        Perform a Service Status Query via ControlCenterServicesPort service on CUCM given the service_name

        This API method executes a soapGetServiceStatus Request and sets results with returned Response data

        https://developer.cisco.com/docs/sxml/control-center-services-api/#soapgetservicestatus

        """
        try:
            cucm_service_status_query_parsed_args = cucm_service_status_query_args.parse_args(request)
            service_list = None
            if cucm_service_status_query_parsed_args['Services'] is not None:
                services_str = cucm_service_status_query_parsed_args['Services']
                service_list = list(map(str.strip, services_str.split(',')))
            ccsresult = mySXMLControlCenterServicesService.ccs_get_service_status(service_list=service_list)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "Service(s) Status Info Retrieved Successfully",
                        'service_info': ccsresult}
        return jsonify(apiresult)


@api.route("/perfmon")
class cucm_perfmon_api(Resource):
    # CUCM Perfmon Query API Payload Model
    cucm_perfmon_post_data = api.model('perfmon_post_data', {
        "perfmon_class": fields.String(description='Performance Class Name', example='Cisco SIP Stack', required=False),
        "perfmon_counters": fields.List(fields.String(description='Performance Counter Class + Instance + Name',
                                                        example='Cisco CallManager\\RegisteredOtherStationDevices', required=False))
    })

    @api.expect(cucm_perfmon_post_data, validate=True)
    def post(self, perfmon_class=None, perfmon_counters=None):
        """
        Query Performance Counters via PerfMon service on CUCM

        This API Method needs to be a POST method even though we are not creating any new items/resources because Swagger UI does
        not allow a payload body for GET requests.

        This API method executes multiple PerfMon API requests to get the Performance Counters values and sets results with returned Response data

        https://developer.cisco.com/docs/sxml/perfmon-api/

        """
        try:
            # If no perfmon_class and perfmon_counters are passed via api.payload and via post method arguments, we can't proceed
            if not ('perfmon_class' in api.payload or 'perfmon_counters' in api.payload) and not (perfmon_class or perfmon_counters):
                raise Exception(f"perfmon_class or perfmon_counters is required in the payload or post method arguments")

            # If perfmon_class is NOT defined via post method argument but its in the api.payload then use it
            if not perfmon_class and api.payload.get('perfmon_class'):
                perfmon_class = api.payload.get('perfmon_class')
            if perfmon_class:
                perfmon_class_result = mySXMLPerfMonService.perfmon_query_class(perfmon_class_name=perfmon_class)
            # If perfmon_class is still not defined then set result to None
            else:
                perfmon_class_result = None

            # If perfmon_counters is NOT defined via post method argument but its in the api.payload then use it
            if not perfmon_counters and api.payload.get('perfmon_counters'):
                perfmon_counters = api.payload.get('perfmon_counters')
            if perfmon_counters:
                perfmon_session_handle = mySXMLPerfMonService.perfmon_open_session()
                perfmon_counter_list = []
                for counter in perfmon_counters:
                    perfmon_counter_list.append(f"\\\\{cucm_host}\\" + counter)
                if not mySXMLPerfMonService.perfmon_add_counter(session_handle=perfmon_session_handle, counters=perfmon_counter_list):
                    mySXMLPerfMonService.perfmon_close_session(session_handle=perfmon_session_handle)
                    raise Exception(f"Failed to Query Counters: {api.payload['perfmon_counters']}")
                perfmon_counters_result = mySXMLPerfMonService.perfmon_collect_session_data(session_handle=perfmon_session_handle)
                if any(("%" in counter) or ("Percentage" in counter) for counter in perfmon_counters):
                    sleep(5)
                    perfmon_counters_result = mySXMLPerfMonService.perfmon_collect_session_data(session_handle=perfmon_session_handle)
                mySXMLPerfMonService.perfmon_close_session(session_handle=perfmon_session_handle)
            # If perfmon_class is still not defined then set result to None
            else:
                perfmon_counters_result = None

        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)

        apiresult = {'success': True, 'message': "PerfMon Data Retrieved Successfully",
                        'perfmon_class_result': perfmon_class_result,
                        'perfmon_counters_result': perfmon_counters_result}
        return jsonify(apiresult)


@api.route("/apply_phone/<string:device_name>")
@api.param('device_name', description='The Name of the Phone Device')
class cucm_apply_phone_api(Resource):
    def put(self, device_name):
        """
        Applies a Phone Device Configuration on CUCM
        """
        try:
            axlresult = myAXL.apply_phone(device_name)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "Phone Configuration Applied Successfully", 'uuid': axlresult['return']}
        return jsonify(apiresult)


@api.route("/phones")
class cucm_list_phone_api(Resource):
    @api.expect(cucm_list_phones_search_criteria_query_args, cucm_list_phones_returned_tags_query_args, validate=True)
    def get(self):
        """
        Lists all provisioned phone details from CUCM

        This API method executes listPhone AXL Request with the supplied search criteria
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_listPhone.html#Link986

        """
        try:
            list_phones_search_criteria_query_parsed_args = cucm_list_phones_search_criteria_query_args.parse_args(request)
            list_phones_returned_tags_query_parsed_args = cucm_list_phones_returned_tags_query_args.parse_args(request)
            returned_tags = None
            if list_phones_returned_tags_query_parsed_args['returnedTags'] is not None:
                returned_tags_str = list_phones_returned_tags_query_parsed_args['returnedTags']
                returned_tags = list(map(str.strip, returned_tags_str.split(',')))
            axlresult = myAXL.list_phone(search_criteria_data=list_phones_search_criteria_query_parsed_args,
                                            returned_tags=returned_tags)
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return jsonify(apiresult)
        apiresult = {'success': True, 'message': "Phone List Retrieved Successfully",
                        'phone_list_count': len(axlresult['return']['phone']),
                        'phone_list_data': axlresult['return']['phone']}
        return jsonify(apiresult)


@api.route("/line/<string:directory_num>")
@api.param('directory_num', description='The Line Directory Number')
class cucm_line_api(Resource):
    @api.expect(cucm_update_line_query_args, validate=True)
    def put(self, directory_num):
        """
        Updates a Line Number configuration and Applies it on CUCM

        This API method executes an updateLine and applyLine AXL Requests with the supplied phone_update_data parameter
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_updateLine.html
        <br>
        https://pubhub.devnetcloud.com/media/axl-schema-reference/docs/Files/AXLSoap_applyLine.html
        """
        try:
            cucm_update_line_query_parsed_args = cucm_update_line_query_args.parse_args(request)
            callforwardtovm = cucm_update_line_query_parsed_args['callforwardtovm']

            line_update_data = {
                "pattern": directory_num,
                'callForwardBusy': {'forwardToVoiceMail': callforwardtovm},
                'callForwardBusyInt': {'forwardToVoiceMail': callforwardtovm},
                'callForwardNoAnswer': {'forwardToVoiceMail': callforwardtovm},
                'callForwardNoAnswerInt': {'forwardToVoiceMail': callforwardtovm},
                'callForwardNoCoverage': {'forwardToVoiceMail': callforwardtovm},
                'callForwardNoCoverageInt': {'forwardToVoiceMail': callforwardtovm},
                'callForwardOnFailure': {'forwardToVoiceMail': callforwardtovm},
                'callForwardNotRegistered': {'forwardToVoiceMail': callforwardtovm},
                'callForwardNotRegisteredInt': {'forwardToVoiceMail': callforwardtovm}
            }
            axl_update_line_result = myAXL.update_line(line_data=line_update_data)
            myAXL.apply_line(directory_num, 'DN_PT')
        except Exception as e:
            apiresult = {'success': False, 'message': str(e)}
            return apiresult
        apiresult = {'success': True, 'message': "Line Configuration Updated & Applied Successfully",
                        'uuid': axl_update_line_result['return']}
        return apiresult