The Cisco Webex Suite, along with other Cisco Collaboration products like Cisco Unified Communications Manager (CUCM) — which offers call control services for Cisco Webex Calling Dedicated Instance — provides a comprehensive set of Application Programming Interfaces (APIs). These APIs can be used to extend and enhance the product capabilities, management, and diagnostics. This course will guide you through commonly used APIs in the Cisco Webex Suite and CUCM, enabling you to programmatically control messaging, meeting, and calling capabilities, including provisioning tasks common to the Suite.
The goal of this lab is to give you hands-on knowledge of how to use these APIs by starting from the basics and building up to more complex tasks. By the end, you will use these APIs to build a simple web-based management portal that will enable you to simplify a series of complex provisioning and monitoring tasks, such as moving a user from UCM calling to Webex Calling.
Let's walk through a few use cases:
These are just some of the examples of problems that can be solved using APIs. We will explore some of these challenges in this lab and walk you through how to use the APIs to solve them. The intention of this lab is not to make you an expert in any one API, but rather to give you a broad understanding of the capabilities of the APIs and how to use them to solve common problems so you can adapt the knowledge to your own use cases.
To be clear, this lab is not a course about web development or programming in Python, Javascript, or any other language. Although you will use Python and some Javascript, we have provided enough information for those unfamiliar with these languages to complete the tasks. Because of the limited time for this lab and our desire to provide you with as much information as possible, you will see that we have provided you with code to lay the foundation for what you will be building and your tasks will largely be to fill in the important parts of the code to make it work. For your final task in the lab, a basic web page (HTML/CSS/Javascript) has already been created for you and your focus in this lab will be to leverage the collaboration product APIs to accomplish the tasks the portal provides.
Cisco's collaboration prodcts generally use one of two types of APIs: SOAP and Representational State Transfer (REST). Most modern APIs are based on the REST architecture, but most of the on-premises Cisco Collaboration products (including Webex Calling DI) use SOAP so it is important to learn both. We will start with an overview of the two types of APIs and then dig into the specifics of how this applies to the various product APIs in particular. We will walk through some of the tools available to interact with the APIs, how to use the documentation, and how tooks like the AxlToolkit and Webex Software Development Kit (SDK) can help you build applications without having to worry about some of the details of the API.
As mentioned, this is also not a class about learning Python. We selected Python due to its popularity, readability, and the abundance of external libraries and Software Development Kits (SDKs) like wxc_sdk . If you've had some experience with Python, even as a novice, the concepts introduced here shouldn't be overly complicated. If you're new to Python, don't worry; we've provided enough guidance for you to complete the tasks with little to no prior knowledge.
In Python, indentation and spacing are essential for defining code structure, replacing the need for curly braces or keywords. As you complete the lab, ensure your code is properly indented, using comments or existing code as alignment guides. Correct indentation is crucial for your code to work correctly.
For all Python coding in this project, you will use Visual Studio Code (VS Code) , a popular free source code editor from Microsoft. You will edit and run your Python code in this lab environment on a remote Linux development server. Your Pod's dedicated development server has a web-based VS Code instance running on it. This web-based VS Code application is called code-server and behaves identially to VS Code running locally on your PC. You can find more information about VS Code, code-server, and your development environment in the Reference section of this lab guide.
To begin, you will launch VS Code and run a simple Python program by following the steps below.
One of VS Code's features is the ability to run applications from within the development environment.
Congratulations on successfully running your first Python program in Visual Studio Code! You are now well-equipped to delve into the world of Cisco Webex APIs.
Cisco collaboration products make use of one of the two most prevelant web services types, REST and SOAP. We will start with SOAP and then move on to REST in later sections. Most of the Cisco Unified Communications Manager (CUCM) APIs (Administrative XML (AXL), UC Manager Serviceability (SXML), Platform Administrative Web Services (PAWS), etc) are exposed via SOAP-based XML Web Services, which conform to the Simple Object Access Protocol (SOAP) Specification 1.1 and the Web Services Description Language (WSDL) Specification 1.1 . SOAP provides an XML-based communication protocol and encoding format for communication.
If you are not familiar with XML, it stands for eXtensible Markup Language and is a markup language that defines some relatively simple rules for encoding data. It was primarily designed to transmit and receive structured data in a well-defined format both systems can understand. In it's most basic form, XML defines tags which are enclosed in angle brackets (<>) and these tags surround the data described by the tag. Tags can form a hierarchy with tags inside of other tags. For example, to define a basic phone device, you might say that a phone device needs three parameters, a name, a description, and a phone number. To describe such a phone using XML, you would define the following structure:
Notice how for each tag, there is a corresponding end tag that includes a forward slash (/ before the tag name).
The names of the tags can, in theory, be any arbitrary string. In order for two devices using XML to agree upon what tags should be present and what type of data is expected between those tags, an XML schema is defined. An XML schema describes in painstaking detail not only the names of the tags, but also what order they should appear, what kind of data can be enclosed between the tags (for example a string, an integer, or some other sequence of tags), whether specific tags are mandatory, or if a particular tag can repeat more than once. Let's take a look at one of the simpler objects that can be configured on CUCM, a Partition. The following is a piece of the schema definition for a partition (called a RoutePartition in the API):
You can see that the definition of a Partition includes a name, description, timeScheduleIdName, and timeZone. The type field tells you what kind of data is in that field. The values that are a string are self-explanatory, but what is a XFkType or XTimeZone ? To determine what these custom types are, you must find them in the schema definition, as they have their own set of rules. You will find that there will be many references to other parts of a schema and those other parts can further reference other parts.
XML schemas define how to describe an object or API call using XML. For example, you can describe a phone or a partition by following an XML schema. In order to perform transactions using these objects, you need something like SOAP which describes how to send requests and receive responses using XML messages.
The following diagram shows you the structure of a SOAP message:
What does this look like when encoded in XML?
The SOAP message above is a very basic request for the CUCM version. You can see that the getCCMVersion request has no parameters as evidenced by lack of any tags inside the getCCMVersion tag. The SOAP standard describes the above structure for a SOAP message. If you are thinking to yourself that this is getting complicated, you can relax. We are showing you what is happening behind the scenes, but as you will see shortly, there are many tools that will abstract this structure from you so that you don't have to deal with it.
If an XML schema describes the messages and objects that can be defined using XML, then how do you know what types of requests you are allowed to make, what types of data those requests require, and what type of response you expect to receive? This is where the Web Services Description Language (WSDL) comes into play. A WSDL file (along with associated XML schema files) can be used to fully describe the capabilities of a SOAP API including all requests and responses that can be made and the data types that are expected.
A WSDL file defines four pieces of data:
CUCM provides a WSDL file for each of the SOAP-based APIs it supports. When trying to use these SOAP based APIs, interpreting the WSDL files and then attempting to cross-reference all the types in the schema files can be a daunting task. Fortunately, there are tools to read WSDL files and then make the SOAP API service methods available easily. The eventual goal is to leverage a programing language such as Python to interface with the various SOAP API's, but it helps to manually explore the API using a visual tool that can understand the WSDL file to get a better appreciation for what is happening when interacting with SOAP APIs. One of these tools is SoapUI which has been installed on your workstation.
The first API you will explore is the Administrative XML Layer (AXL) API. The AXL API is a provisioning API for CUCM described in much greater detail in the next section. AXL allows you to configure anything that you would be able to configure from the CUCM Admin web page.
Perform the following steps to get started with SoapUI.
The WSDL and XML schema files for the AXL API are published on the CUCM server itself, as part of the Cisco AXL Toolkit plugin. You will need these for SoapUI to be able to interact CUCM's AXL web service.
Now you can load this WSDL into SoapUI, get things configured, and start sending queries. Follow these steps to load the WSDL into SoapUI.
Once the API is loaded, you must set some of the default parameters, specifically the CUCM hostname or IP address and the credentials so that they don't have to be re-entered for every query.
SoapUI is now all set up for issuing queries. When you opened the WSDL file in SoapUI, SoapUI parsed the contents of the file in addition to the XML schema files that are referenced in the WSDL to provide you with a list of all the actions you can take using the AXL API. You will notice that the Navigator pane on the left contains a list of all the AXL API methods available in the API. Just as all these methods are now available to you at the click of a button, you will see later how certain libraries can perform a similar action in Python to read the WSDL file and make all of these methods available to you programmatically. To perform your first AXL SOAP API call, follow the steps below:
You have successfully sent an AXL/SOAP request to CUCM and received a valid response. This request was fairly basic as you did not have to provide any parameters in the request. Even with only one piece of information in the response, the version of CUCM, the response seems fairly complex. Having to manually parse and extract the data from the XML-based response may seem daunting. You will see later when you perform similar actions using Python that the SOAP library you will be using will greatly simplify the process of extracting the data from the response.
In the next section, you will explore the AXL API in more detail and construct some more complex requests.
Administrative XML Web Service (AXL) is an XML/SOAP based API that provides a mechanism for inserting, retrieving, updating and removing data from the CUCM configuration database. The AXL API provides you with access to the entire CUCM database. The AXL API is purely for provisioning and does not provide access to run-time or performance data. There are several other APIs we will discuss later that are used to provide this type of information.
Before exploring more of the AXL API using SoapUI, here are a few things you should know about the API:
AXL API utilizes HTTPS Basic Authentication. Any CUCM User (Application or EndUser) will have read/write access via AXL if they are members of the Standard CCM Super Users access control group or any group with the Standard AXL API Access role assigned to it. This means that all super user accounts implicitly already have access to the AXL API. To create an account dedicated for AXL API use, you must first create an access control group and assign the Standard AXL API Access role to it, then associate the application user with the newly created group.
To provide read-only AXL API access you can create a separate Access Control Group and assign only the Standard AXL Read Only API Access role to it.
In the interests of simplicity in this lab, you will use the admin account which is a member of the Standard CCM Super Users group. However, the best-practice in a production environment is to use a separate account dedicated for each AXL API application.
AXL has two distinct method types which operate in very different ways.
AXL API has built-in supports versioning for Thick AXL API requests only. Thick AXL API requests can specify the AXL schema version in the request header SOAPAction: CUCM:DB ver=15.0 as well as in the XML namespace URI for the the SOAP Envelope element within body of the request xmlns:ns="http://www.cisco.com/AXL/API/15.0" . Both SOAPAction header and the XML namespace URI should indicate the same version. The SOAPAction request header is optional but if supplied it supersedes the version specified in the XML namespace URI. The AXL schema version in the XML namespace URI is always required.
The version specified in the AXL API request indicates which AXL schema version the request payload will follow and the response should follow.
CUCM maintains backwards compatibility with the running release minus 2 major versions. For example, applications written with CUCM 12.5 schema will still be supported through CUCM version 15 (because there was no version 13, so the next two versions are 14 and 15).
Although not recommended for schema dependant Thick AXL methods such as addPhone or updatePhone, if an AXL API client wishes not to specify a specific schema version, for example, when using the getCCMVersion method, we can omit the SOAPAction header and include a particular schema version 1.0 in the AXL XML namespace URI. In this case, CUCM will attempt to apply the lowest schema version supported by that CUCM release. For example: The AXL Developer Guide - Versioning on developer.cisco.com has more information including supported schema versions per CUCM major release as well as backward compatibility best practices and more.The AXL SOAP Service has dynamic throttling to protect system resources. When receiving an AXL write request, the CUCM publisher node via Cisco Database Layer Monitor service dynamically evaluates all database change notification queues across all nodes in the cluster and if any one node has more then 1500 change notification messages pending in its database queues, the AXL write requests will be rejected with a "503 Service Unavailable" response code. Even if the CUCM Cluster is keeping up with change notification processing and DB queues are NOT exceeding a depth of 1500, only 1500 AXL Write requests per minute are allowed .
The database change notifications queue can be monitored via the following CUCM Performance Counter on each node ( \\cucm\DB Change Notification Server\QueuedRequestsInDB ). This counter can be viewed using the Real Time Monitoring Tool (RTMT), but we will also show you how to retrieve CUCM Perfmon Counter values programmatically as well in the next section.
AXL read requests are NOT throttled even while write requests are being throttled.
In addition to AXL requests throttling, the following AXL query limits are always enforced: A single request is limited to returning under 8MB of data. Concurrent requests are limited to under 16MB of data.
The Data Throttling and Performance page on developer.cisco.com has more information on this topic.
As mentioned earlier, XML objects are defined in an XML schema file. The latest CUCM AXL Schema Reference (CUCM 15) can be found at https://developer.cisco.com/docs/axl-schema-reference/ . This guide shows you the detailed structure of hundreds of methods available as part of the AXL API.
To reference an older version of the AXL schema, refer to https://developer.cisco.com/docs/axl/#archived-references .
You may refer to the AXL Operations by Release Matrix located at
https://developer.cisco.com/docs/axl/#!operations-by-release
to identify changes between Releases.
Now, with a background on the AXL API, you can use the SoapUI application to perform a common task via the API - adding a Phone to CUCM. At this point you are using the SoapUI application to construct the raw XML messages that will be transmitted to CUCM. Later you will do this in an easier, more programmatic way.
The same optional input element is documented in the AXL Schema for AddPhoneReq/phone element as shown below. Notice how the box around the word description has a dotted line. This indicates that parameter is optional:
The response data above contains the UUID (Universally Unique IDentifier) for this Phone device just added to the CUCM database, which uniquely identifies this device in the database. It may be useful for modifying or removing this device. Within the CUCM Database, this is how this phone device will always be referenced. You can actually see this same ID if you log into the CUCM admin and, from the Device > Phone page, hover over the link to this phone. The URL would be something like: https://cucm1a.pod22.col.lab/ccmadmin/gendeviceEdit.do?key= 06ea28c2-18ef-ecce-9ad1-e47f41c7e46f . In practice, if you are using Thick AXL, you should not need the UUID because you can always reference the device by the device name for any additional queries on the device.
The response contains a SOAP Fault message indicating that the phone device could not be added because a device with the same name already exists in the CUCM database. In this case the error is expected because you are trying to add the same device twice, so you can ignore this error.
Now that you have your phone registered to CUCM using AXL Methods with the SoapUI tool, let's look into a few more AXL Methods but in a programmatic way using Python.
Now that you have discovered how to interact with the AXL API by using SoapUI to execute against the WSDL-defined API methods, you may be wondering how to overcome the challenge of programmatically building these requests' payloads and parsing their responses. Good news: this is where a Python library named zeep comes to our aid. The Zeep library, which can be downloaded from https://pypi.org/project/zeep/ , itself depends on a few more Python libraries, such as lxml and requests to list a few. Collectively these Python libraries make our automation development efforts much easier. We will gently introduce you to these Python libraries before diving deep into them as you build the provisioning portal.
You will now navigate to your development environment where a web-based VS Code is running. You will start with executing the same listPhone AXL method that you used with SoapUI, but this time you will use Python code. The six tasks you will accomplish in this section are:
Follow these steps to perform the above listed tasks:
Start with importing the zeep library to the sample Python script. Click the Copy button next to the highlighted line and paste it into your file after the line that has the comment saying "Import zeep library".
Next, create a few constant variables that you will use to connect and authenticate to your Pod's CUCM AXL API. NOTE that this is just an example for learning purposes. In a production environment, you would not hard code these values into your script.
Copy and paste your Pod's CUCM hostname, admin username and password into your file.
In this lab environment we are not using CA signed (valid) certificates on the CUCM server. In order to allow the Python script to establish SSL connection you need to disable SSL certificate verification for the sessions. Disabling SSL certificate validation is not recommended in production environments, hence there will be a warning printed in your console every time an insecure request is made. You will disable this warning as well.
Finally you will configure the authentication type and credentials that will be used throughout the session. The script will utilize the HTTP Basic authentication method and the CUCM_USERNAME and CUCM_PASSWORD variables that you have created earlier.
The following block of code will first create the session instance, then disable SSL Certificate verification and configure basic authentication. Paste the following highlighted lines it into your file:
Paste the following into your file:
Now that you have created this service object you can use it to access any of the methods defined in the WSDL file such as the listPhones method. Follow these steps to execute the listPhones method to retrieve a list of phones.
The listPhone method requires the following arguments: searchCriteria and returnedTags . The methods you call on the Zeep service object have the same names as what you see in SoapUI, so if you want to call the listPhones method, you would call service.listPhone with the appropriate arguments. The sample request from SoapUI helps you understand what parameters to pass without having to read through the detailed documentation.
When using Zeep, you do not need to worry about the XML structure of the SOAP request. Zeep will take care of that for you. Instead, you will pass basic Python data types such as numbers, strings, or dictionaries to the service.listPhone method depending on what is needed and Zeep will convert them to the appropriate XML structure. In this case, these two parameters require dictionaries because they are complex data structures that have key value parameters. Create the following variables that you will pass as arguments to the listPhones method. Paste the following into your file:
The first log will print the raw request payload that is sent to the CUCM while executing the service.listPhone method.
The next log will print the raw response payload that is received from the CUCM in response to the listPhone request
The last log will print the parsed response by zeep library. Paste the following into your file.
In the VS Code navigation bar on the left, click the Run and Debug button.
Once you click anywhere in list of "RUN AND DEBUG" configurations, You should see a predefined configuration for running a python script named Python: soap_axl.py in the list. Make sure it is selected:
You will notice the list_phone_response variable content is significantly larger then the response SOAP payload received from CUCM. This is a function of the zeep library parsing the response against the AXL schema. As the code requested, only the name and the uuid keys will have a value assigned to them. The rest of the optional returnedTags will have a None value assigned to them. You will utilize this behavior to your advantage later when using AXL methods that require complex data structures to be passed in, such as updatePhone or addPhone for example.
Earlier you added a phone and registered it with CUCM, but the test call failed when you attempted to dial to voicemail because no Calling Search Space was configured when the phone was added. In this step you will create a request to update the existing phone and add the Calling Search Space.
This payload should look very similar to what you saw when you performed an addPhone with SoapUI, with the exception that nearly every parameter is now optional. Notice that you are given a choice of providing either a name or a uuid to identify the device you wish to modify. The one parameter that is present here which is not present for adding a phone is the newName parameter. This would be used if you want to rename the device. Any parameter that is not sent in the request will retain whatever setting was present for that parameter before the update call. This helps you distinguish between setting a parameter to an empty string versus keeping the existing value. For example, if you were to send <description></description> as one of the fields, this would clear the description, but if you do not send a description tag in the request at all, the description would not be modified.
Create a new variable that will supply the required arguments to the updatePhone method. The correct calling search space for your phone is Unrestricted_CSS .
You may notice you are not passing update_phone_data directly to the updatePhone method. This method expects name and the callingSearchSpaceName as arguments. You can achieve this by unpacking the update_phone_data dictionary to its key,value pairs with the ** unpack operator.
In other words, instead of service.updatePhone(**update_phone_data) with the name and callingSearchSpaceName specified in the update_phone_data dictionary, you could have just as well entered service.updatePhone(name='CSFPOD22UCMUSER', callingSearchSpaceName='Unrestricted_CSS') . When passing in a small number of parameters, this might be more convenient, but if you are updating a large number of parameters, the method we show above is typically easier to manage.
For debugging purposes, the three log statements log the outgoing and incoming request payloads as well as the parsed response data.
If the end of the console output looks as following, you have successfully updated the CSFPOD22UCMUSER device's Calling Search Space.
Modifying a CUCM Phone device configuration is pretty straightforward, but if you have worked on or administered Unified CM before, you might recall that device setting changes require either a device reset or restart. Normally from Unified CM Administration you would click the Apply Config button to trigger the phone to download its latest configuration and let it decide if it needs to go through a full on reset or a quick restart depending on the configuration update. You can perform this same procedure by using the AXL API as shown below.
If the end of the console output looks like the following, you have successfully applied your config update to the CSFPOD22UCMUSER device.
As you gain experience with the AXL API, you will often find that the easiest way to add a complex configuration such as adding a phone is to first retrieve the existing configuration for the object you are trying to add, modify it, and then use the add method to add the device. For example, to add a phone you would first use the getPhone method to get the details of a phone that is already configured, modify the returned data structure to your needs, and then use the addPhone method to add the new phone.
The next AXL method you will use is the getPhone method. You will use its response to help craft the Python arguments for the addPhone operation you will perform next, where you may recall from SoapUI could take quite large and complicated arguments. The getPhone AXL method will retrieve the full phone configuration details.Here is a sample getPhone request that SoapUI generated. As a learning exercise, you will retrieve all the possible returnedTags. Basically, you will query for the complete phone configuration by not requesting any returnedTags.
If your last console output looks like the following trimmed output, you have successfully retrieved all of your Phone device configuration for CSFPOD22UCMUSER . Both the SOAP response payload and the parsed response is quite large.
The final AXL method you will execute is addPhone. You will use the previous getPhone response's parsed output to craft the variables that you will pass as arguments to addPhone method. The required parameters for the addPhone method are name , product , class , protocol , protocolSide plus any optional parameters you would like to configure.
If you take a closer look at the callingSearchSpaceName key, it's value is yet another dictionary with two keys _value_1 and uuid . You are not required to supply both of these keys to configure the new phone's callingSearchSpaceName. As you create a new variable named add_phone_data, you will simply set the callingSearchSpaceName key's value to the name of the calling search space which is found in the _value_1 key's value as seen above. For example:
You will find many other parameters returned from the various "get" methods return both the name and the UUID for your convenience, but when doing an add or update, you only specify one.
If your last console output looks like the following output, you have successfully add a new phone.
Now that you have added a new test phone named CSFPOD22USER2 , you can check that it is in actually inserted in the CUCM database.
The next series of CUCM APIs you will experiment with in Python are all part of a collection of APIs known as the UC Serviceability XML API that expose multiple capabilities for CUCM administrators to automate management and monitoring tasks.
For your reference, the family of UC Serviceability XML APIs consist of the following 5 APIs:
Each of these APIs has its own dedicated WSDL file that describes its unique methods (service endpoints) and unlike AXL (AXLAPI.wsdl) they are available to download directly from a given CUCM server. Note that all methods (service endpoints) are NOT available in all CUCM releases. See https://developer.cisco.com/site/sxml/discover/overview/ for more information.
Let's take a closer look at the RisPort70 and Perfmon APIs with a sample Python script.
The tasks you will accomplish in this section are:
Follow these steps to perform the above listed tasks:
Copy and paste your Pod's CUCM hostname, admin username and password into your file.
The RisPort API allows you to query CUCM for the real-time state of registered devices. This is different than AXL which provides you with the static database configuration of devices. With the RisPort API you can retrieve information such as registration status, IP address, running firmware load, and more. To retrieve this data you will use the selectCmDevice method of the RisPort API as shown below.
To select any device whose description begins with "Cisco", you must provide the following values as part of the CmSelectionCriteria data in the selectCmDevice request:
The values above indicate that you want to retrieve any kind of device, any model of device (255 means any model), devices that are in any status (Registered, Unregistered, etc...), registered to any node in the cluster, registered using any protocol (SCCP or SIP), and select the devices whose description starts with the word "Cisco".
The XML below shows what the expected SOAP request to CUCM should look like. As before, you do not have to manually create this XML body. Zeep will take care of doing this for you.
To pass the selection criteria to the RisPort API, first create the following variable that you will use to pass as an argument to selectCmDevice method. Paste the following into your file.
In VS Code navigation bar on the left, click the Run and Debug button.
Once you click anywhere in list of "RUN AND DEBUG" configurations, You should see a predefined Configuration for running a python script named Python: soap_sxml.py in the list. Make sure it is selected:
The selectCmDevice response parsed by the zeep library looks as follows. The output is quite large since you are retrieving all device details from the RIS DB. For your reference please find a trimmed output of phone_query_response below. Your console output should look slightly different but should show the CSFPOD22UCMUSER device in a registered state.
In the previous step, you used the RisPort70 serviceability API to query the RIS DB for devices that matched a specific selection criteria. Next you will use the PerfMon API to query a simple set of CUCM Performance counters. If you are unfamiliar with PerfMon, it is a service available on all CUCM servers to provide real-time counters for the various services running on the server. These metrics include everything from CPU and memory utilization to number of registered devices and active calls. PerfMon has a series of objects which are logical groupings of related counters. Counters provide a single metric related to the object. For example, the Cisco CallManager object contains a variety of counters related to the Cisco CallManager service such as registered phones, active calls, and the uptime of the service. An easy way to browse the various objects and counters available is by using the Real Time Monitoring Tool (RTMT).
The perfmonCollectCounterData method requires the following arguments: Host and Object . The perfmonCollectCounterData documentation https://developer.cisco.com/docs/sxml/perfmon-api/#perfmoncollectcounterdata can help identify the arguments and the data structure you must provide to the service.perfmonCollectCounterData method.
In VS Code navigation bar on the left, click the Run and Debug button.
Once you click anywhere in list of "RUN AND DEBUG" configurations, You should see a predefined Configuration for running a python script named Python: soap_sxml.py in the list. Make sure it is selected:
The perfmon_query_response response parsed by the zeep library should look similar to the output below. The output is quite large since you are retrieving all PerfMon Counters for the "Cisco CallManager" class. For your reference please find a trimmed output of perfmon_query_response.
The method of using the PerfMon API highlighted above is the simplest use of the API. Most use cases involve continuously querying the same performance counters at a certain time interval and storing them, perhaps as a time-series in a database of some kind. To do this, the PerfMon API provides a mechanism by which you open a session, add counters to that session, then periodically collect session data for those counters. For more information on the more advanced usage, reference the PerfMon API Reference and look at how to use the perfMonOpenSession , perfMonAddCounter , and perfMonCollectSessionData methods.
You have now completed both the AXL and Serviceability XML APIs examples with Python. In the next section you will be introduced to REST-based APIs.
REST , which stands for RE presentational S tate T ransfer, is primary type of API used in Cisco's Webex Suite products. Other collaboration products such as Unity Connection and Cisco Meeting Server also use REST APIs. Now that you have explored SOAP APIs, you will see that REST APIs are very different than REST APIs. Unlike SOAP, there is no "REST standard" per-se. While SOAP is a well-defined protocol, REST APIs are a style that leverage existing standards such as HTTP, JSON, XML, and others to provide a web service. As a result, you will notice differences in how different products implement REST APIs. This lab will expose you to how Webex implements REST APIs. The good news is that different features in Webex are generally consistent in how they implement their APIs, so once you learn how to use one Webex API, you will have a good idea of how to use others.
REST-compliant (RESTful) systems consist of a client and server that exchange stateless transactions. Although technically not mandatory, all REST-based APIs in Webex use HTTPS. In this model, a client sends a request to a server; the server processes the request and sends a response back to the client.
The type of request is called a verb and there are four that are covered in this lab:
This request has a path to a resource , which is represented as a portion of the URI following some base URI that is common to all requests for a given API. It may also have query parameters after the URI, indicating some variable or setting. The ? character marks the end of the URI (the resource) and the start of these parameters. Each additional parameter is separated by the & character. For example:
In this example, all API requests to this API will start with https://host/api/v1/ followed by the resource (which can have additional path elements) and then any query parameters. This structure is no different than any other web request you might make in a browser when navigating to a web page so it should feel familiar to anyone who has paid attention to the URL in their browser.
Responses to a request include a status code and a body . The status code is a three-digit number that indicates the result of the request. The body is the data that is returned by the server. The body is typically in JSON or XML format, but can be any format that the server supports. The body of the response is typically the most important part of the response for a GET requests because it contains the data you requested. The status code is important because it tells you if the request was successful or not.
Like any other response from a web server, the status codes are divided into ranges. The most common status codes you will see are in the 200 range which indicate success. Responses in the 400 range indicate a client error - typically that the request was malformed or that the client does not have permission to access the resource. A 404 Not Found is a common example of a client error indicating that the resource you requested does not exist. Responses in the 500 range indicate a server error - typically that the server was unable to process the request for some reason.
A REST request will also include Headers , for things like authentication and defining the data format of the payload, if any. Headers play an important role in authentication in REST APIs - specifically the Authorization header. Following the Headers is the body of the request, if any. The body is typically used in POST and PUT requests to send data to the server. The body is typically in JSON or XML format, but can be any format that the server supports. For Webex APIs, these will typically be in JSON format.
RESTful APIs may implement authentication and authorization in a variety of ways, or not at all. For example, the Cat Fact API at https://catfact.ninja/fact will return a JSON formatted body containing a random cat fact and doesn't require any authentication. Because no authentication is required and this particular API endpoint uses the GET verb, you can access it with a standard web browser. This is an example of a very simple REST API. As you will see, the APIs available in Webex are more complex than this, but they fundamentally work in the same way.
While different Cisco collaboration products that expose RESTful APIs may use different authentication mechanisms, Webex APIs consistently use one form of authentication - access tokens. Usernames and passwords are never used to authenticate to Webex APIs. This is not to say that you may not need a username and password to obtain an access token, but the access token itself is what is used to authenticate to the API.
The REST APIs provided by Cisco Webex use access tokens for authenticating and authorizing API requests by leveraging the OAuth2 standard . Access tokens can come from a variety of sources, but they are all used in the same way: the HTTP Authorization header contains the word Bearer followed by a long string of characters that represents the access token. For example, the Authorization header for request may look something like this:
Authorization: Bearer CDc4KmFjMzAtMDA9jjauYWUkKWExMeEtYmZ3MmZhMGNiN2VjYjab_AD92_039cfbf8-2732-8f82-0239-7283abef6612
The contents of the access token itself can vary from implementation to implementation. Some APIs use opaque tokens which means that the token itself is just an identifier and the system to which you pass the token must ask the authentication system whether the token is valid or not and what permissions the token has access to. Another token format is the JSON Web Token (JWT) format (pronounced "jot"). JWT tokens are self-contained and provide all the authentication information in the token itself and are cryptographically signed in a way that the server authenticating the request can validate that the token is valid without having to ask the authentication system. Webex APIs make use of both opaque tokens and JWT depending on the mechanism by which you obtain the token. The type of token is not of much importance to someone using an API because it is up to the service receiving the token to determine how to authorize the user based on the token; however, because JWT are self-contained, you can use a decoder like the one on jwt.io to give you the contents of the token. Be careful using online tools like this because your token is senstive like a password. Someone who has the token can access any resources granted to that token.
Access tokens also typically have a set of Scopes associated with the token. A scope is a way to define a specific permission in the API. Scopes are completely implementation dependent and each API you find that uses tokens will have different levels of granularity in their scopes. For example, one API may just have a "readonly" scope and a "read-write" scope while other APIs may have specific scopes for different levels of access to individual resources - for example a scope that allows for reading only user accounts.
Cisco Webex has a variety of different access tokens available for different needs. You will start here by using a Personal Access Token which is a token that represents your user account in Webex. A Personal Access Token is authorized to do anything you as an individual user are allowed to do. This means that if you are an administrator, the token can perform administrative tasks, but if the token belongs to a standard user, it can only access resources that a non-admin user would have access to. The token you will use in this lab is limited in scope, so in future sections you will explore how to obtain tokens with certain administrative scopes.
One key differences between traditional username / password based authentication and access tokens is that access tokens typically have a limited lifetime. In other words, they expire after a certain amount of time after being granted. Some tokens can expire within minutes or hours while others could be valid for years. Webex Personal Access Tokens are only valid for 12 hours, so after 12 hours, you must generate a new token to continue using the API. If tokens are only valid for a short period of time, how do you stay logged in to an application using access tokens or how can you continue to use an API that requires access tokens without having to provide new credentials? This is made possible through the use of refresh tokens which can be used to request a new access token. We will discuss refresh tokens in more detail in a later section when we dive into service apps and integrations. For now, let's see how you can use a personal access token to make some simple API calls to the Webex APIs.
You can now explore some the features of a REST-based API by way of an example. While there are a number of tools that allow you to construct custom REST queries, such as Postman , the Cisco Webex documentation itself allows you to send API requests directly to Webex and examine the results. As a bonus, it helps you easily construct the request and automatically populates the personal access token authorization for you. Let's take a look at querying a person.
In the email Query Parameter, enter pod22wbxuser@collab-api.com .
Keep in mind, this request and response is from the real Webex system, not a sandbox. Care must be taken when executing some requests, especially those that alter or delete data.
In this example, you see not only a response code (200 OK) as seen in the green box in the upper right corner of the output, but also the Body of the response which contains the user data for the user with this email address (in some cases there is no data returned and the only thing returned is the status code indicating if the request was successful or not). This is a JSON payload, with some indenting to make it more readable. Similar to XML, it is a serialization format. That is, JSON is a way of representing structured data in the form of a textual string, so it can be sent
If you have some Python background, you might notice that it looks very much like a Python dictionary, but it is not exactly the same. Text data in JSON must be enclosed with double quotes (whereas in a Python string, single or double quotes would work), and while Python supports many data types, including custom ones, JSON supports very few (string, integer, array (list), a boolean, null, or another JSON object). Therefore, to change something into a JSON string, you often have to convert to these simpler types. Since this is such a common task, many Software Development Kits (SDKs), which are a set of tools that facilitates usages of specific APIs, will provide methods to export or convert their data in JSON format.
If you are unfamiliar with JSON and would like to learn more, you can find a good introduction here: https://www.digitalocean.com/community/tutorials/an-introduction-to-json or a bit of a more technical introduction here: https://www.json.org/json-en.html .
https://webexapis.com/v1/people?email=pod22wbxuser%40collab-api.com
As you can see, the root URL for the Webex API is https://webexapis.com/v1/ and the resource you are accessing is people . The query parameter email is set to the email address of the user you are looking up. Notice that the '@' symbol is URL encoded as '%40'. This is because the '@' symbol is a special character in URLs and must be encoded to be used in a query parameter. This page lists the various characters that must be encoded in URLs.
If you were to construct an API request using the Python requests library, you would need to know this URL, however as you will soon see, the Webex SDKs will handle this for you.
While sending individual requests to Webex APIs is straightforward, automating tasks programmatically requires careful consideration. Most automation requires multiple API requests in series (or maybe even in parallel and aggregating multiple parallel responses), often using information from one response to construct a new request on a different API endpoint. You must consider the logic necessary to handle a variety of responses including errors and also grant your application access to the APIs while maintaining security. Python offers convenient libraries, such as requests and urllib that allow you to construct and send HTTP(S) requests, but merely constructing requests is insufficient for robust automation. This is not to say that you cannot work with Webex APIs by using a simple HTTP library like these, but you are then forced to deal with challenges such as handling pagination for large datasets and managing response codes indicating congestion, which may warrant retries rather than immediate failure.
To address these complexities, this lab will show you how to use a Software Development Kit (SDK) named wxc_sdk , accessible at https://pypi.org/project/wxc-sdk/ . Initially designed for Webex Calling-specific APIs, this SDK has evolved to encompass the majority of Webex APIs, organized as individual 'packages' within the SDK.
To demonstrate the use of the SDK, start by performing the same user lookup you did earlier using the developer.webex.com documentation, but this time using Python. As with all Webex API interactions in this lab, you will use the wxc_sdk to simplify the task. Follow these steps to get started:
The other code already present in the file performs supporting tasks like setting up the logging infrastructure and contains comments to guide you through the process. If you are unfamiliar with Python and don't understand what these lines do, you can ignore them for now as they are not essential to the task of interacting with the Webex APIs.
To use the SDK in this program, you must first import it by importing the WebexSimpleApi object from the wxc_sdk package. Throughtout this lab you will find examples like what is shown below that indicates the file you must edit, where in the file you should add code, and the code you should add. You can (and should) use the Copy button on the right to copy the selected line or lines and then paste them into your VS Code editor window in the correct location.
In this example, you are looking for the line that says import logging and then adding the import statement on the next line as shown below. Paste the following into your file:
The People API section in the Webex documentation has a corresponding PeopleApi subpackage in the wxc_sdk . This class has the methods list() , create() , details() , delete_person , update() and me() , each one corresponding to a Webex API method.
Now repeat the GET List People query to search for a user with a specific email address, but this time using the SDK instead of the developer.webex.com page. Since the SDK was initialized as api in the previous step, you can perform the exact same query with api.people.list() and pass the email address as a parameter.
Replace the email address in the email_to_search variable with pod22wbxuser@collab-api.com (make sure it is enclosed with quotes):
This for loop iterates through each item in the webex_user_list and assigns it to the variable webex_user . The log.info() function is then called with webex_user as the argument. This will output the contents of the webex_user object to the console.
The webex_user object which represents the single object returned by the SDK is not a simple Python data structure, but rather a custom one called Person in this case. This means you need to treat it differently than a simple Python dictionary as it is an object. If you are not familiar with the difference between the two, for the purposes of this lab you just need to remember that you access the attributes of an object using the dot operator (e.g. webex_user.display_name ) rather than the square bracket notation used for dictionaries (e.g. webex_user['display_name'] ).
You should see a line with " INFO __main__ User information found for " with the display name of the user with the queried email address.
Once again replace the same line of code, but this time with the following:
If you look carefully, the data structure printed matches the Response Properties from the List People method in the Webex API specification exactly (including the keys "id", "emails", "phoneNumbers", "displayName", etc.).
Pay attention to the output. Notice that the output is once again shown in camelCase, not the snake_case that the SDK's Python object uses. Keep this in mind if you ever decide to use the raw JSON returned by the model_dump_json() method. In this lab, we will use the Python object directly.
As seen in the documentation above, this method requires a person_id parameter. If you look at the output of your script, the webex_user that you populated earlier, being an instance of the Person class, has a person_id attribute. Therefore, you can pass the webex_user.person_id as the person_id parameter. Some additional information may be returned if you set the calling_data flag. To be sure, add this as follows:
The output will look basically the same as the previous user dump because, although you specified calling_data=True , there isn't any calling data at this time because calling is not enabled for this user yet. This time through you used the combination of the pformat method and the vars method to dump the object in a more readable format while preserving the snake_case names you must use if you want to access these properties. Printing a returned object in this way is a good way to understand what is being returned without having to look at the SDK documentation.
You might also notice that the phone_numbers attribute is a list of PhoneNumber objects which are also defined in the SDK documentation. This is common with the SDK where you will see objects appear as properties of other objects. While this might seem complicated, once you start working with the SDK, you will find that it is very intuitive and easy to use.
Now that you have completed this basic exmple of using the wxc_sdk to interact with the Webex APIs and retrieve user information, we will move on to leveraging other methods to obtain the access tokens needed to interact with the Webex APIs in a way that is more secure and allows you to use the tokens for a longer period of time. If you recall, the personal access token is only valid for 12 hours. Additionally, you have pasted the token into the Python code which is not a best practice and is not secure.
In previous examples, you used a Personal Access token for all Webex API access. These tokens are great for quick testing, but they are not suitable for production applications because they are only valid for 12 hours. There are several other ways to obtain tokens for use in a production application. Which one you use depends on whose identity and permissions you want to use when interfacing with the API. The following are the different token types available for use in Webex APIs:
The identity of a bot token is always a user id you create in the @webex.bot domain, not your own company's domain. This means that the namespace for bots is global to all of Webex, so if you want to create a bot named, for example, "mybot@webex.bot" you can only do so if no one else has already created a bot with that name. The display name, however, can be anything you want and may overlap with another bot much in the same way that you can have two people named Chuck Robbins as long as they have diferent user IDs.
The following table summarizes the differences between the various token types available in Webex.
How to Get | Identity | Access Scope | Validity | |
---|---|---|---|---|
Personal Access Token | developer.webex.com | The user who generated the token | All permissions available to the user who generated the token | 12 hours |
Integration | OAuth2 Grant Flow (requires web server) | A user who granted permissions to the integration application | Scopes that were requested by the app and granted by the user | 14 day access token + 90 day refresh token |
Service App | developer.webex.com + Control Hub Admin | A machine account with access to a specific Webex org | Scopes that were requested by the app and approved by an org administrator | 14 day access token + 90 day refresh token |
Bot | developer.webex.com | The bot’s own identity (<bot>@webex.bot) | Limited permissions specific to bots | 100 years |
Guest | Generated by a Service App | Guest user | Limited to specific guest permissions | 18 days |
Given the various token generation methods available in Webex, the most appropriate one to use for an application that needs to perform administrative actions in an automated fashion is a Service App . This allows the application to perform administrative tasks without being tied to a specific administrator user in the organization. If you were using an integration or a personal access token. the tokens assigned to the application would be tied to a specific user, so if that user were to leave the company, your application would break. By using a Service App, you remove this depedency. It also allows administrators to delegate only specific permissions (scopes) to the application instead of giving it full administrative permissions. So for example, if you want to build an application that provisions users, you could grant the service app only access to the user provisioning scopes. Similarly, if you wanted to build an application that only uses reporting APIs to run analytics on your Webex data, you could grant the service app only the reporting scopes.
In this section, you will create a service app and use it to obtain tokens that you can use to perform actions your user account does not have access to. Once you have these tokens you can use this for a variety of functions later in the lab. To do this, you will perform the following tasks:
Start by creating a Service App.
That was all the information needed from the Service App. The access_token that was generated is valid, of course, but we do not know exactly for how long. The refresh_token, on the other hand, can be used to generate another access token, which you will do in the sample code.
When your application requests a new access token, Webex will return a new access token, including its expiration time, as well as a refresh token, which may or may not differ from the one used in the request, and its expiration time.
Knowing the exact expiration time of the tokens will allow you to request new ones before they expire, keeping the application up and running.
The output includes that it was able to log in using the Service App access token and query users without encountering an authentication or permissions error
Now that you have an access token with administrative rights, you can continue with enabling a user for Webex Calling.
You have seen how a request to Cisco Webex can be performed using Python and have retrieved an administrative access token via a Service App. Now you can gain more experience by implementing a more substantial business task, such as enabling a user for Webex Calling. Minimally, this involves looking up a user, as you have already done, then updating them with a location, license, and phone number associated with Webex Calling. This will be accomplished by performing the following tasks:
To query for the licenses in your Webex organization, you need to use the List License API. The wxc_sdk has a LicenseAPI submodule so this is similar to looking up a user which you have already done using a similar list() method.
Webex Documentation: https://developer.webex.com/docs/api/v1/licenses/list-licenses
=
You have seen how much of the Webex API documentation maps directly to methods in the wxc_sdk. Another handy way to find a particular API endpoint exposed by the SDK is by using the Reference of all available methods in the SDK. If you look at this guide and search for "api.locations" you will find those methods. In this case, there is an additional method not present in the Webex API, by_name() . This will allow you to look up a location by name, instead of having to get a list of all locations followed by iterating through the list to find the desired location.
Now you have all the data you need to update a person to enable Webex Calling. To do this, you need to take the existing user you retrieved earlier, add the license for Webex Calling, remove the on-prem registration license, modify the location for the user, and then update the user. In our example, you have a user that has their phone number already populated via Directory Connector when they were imported, so you do not have to set it, but if you were doing this in an environment where you don't have the phone number populated, you would have to populate this through the API call as well.
The data returned is all the user information. For Webex Calling, the locationId is the key component. You also see the phoneNumbers as well as a number of licenses licenses entries, although they are only indicated via their object IDs. If calling_data=True were not specified, you would not see any location ID field either.
Now it is time to log into the Webex App with this user and make sure calling is provisioned.
Now that you've worked with Calling-related APIs and the wxc_sdk Python library, it's time to look at the Webex Meetings APIs.
Now that you have explored the Webex Calling APIs, you can take the opportunity to examine the Webex Meetings APIs. Webex provides several APIs that allow you to automate and integrate Webex Meetings into your applications. Some of the things you can do with Webex Meeting APIs include:
We will use this opportunity to also explore another method of obtaining an access token, by using an Integration . An Integration allows an application to implement an OAuth 2.0 flow to authenticate a user in Webex and then obtain an access token with a limited set of permissions, enabling the application to perform certain actions on behalf of a user. In this case, we will use an Integration to schedule a meeting on behalf of a user. This differs from using a Service App, which performs actions on behalf of the Service App itself.
In this example, you will first create an Integration in Webex, then build a simple web application that presents a login button that kicks off the OAuth flow to authenticate the user and obtain an access token. This will allow the web application to create a new meeting on behalf of the user using the Webex Meetings API. You will perform these steps:
The first step to using an Integration is to create one on the Webex Developer Portal. Follow these steps to create an Integration:
Now that you have created the Integration in Webex, you need to configure your code to use the Integration to obtain an access token. Before diving into the code, it helps to understand the process of obtaining an access token using OAuth2 authentication for a Webex Integration. This diagram shows the process at a high level:
The process works like this:
This file implements a simple web server using Python Flask. This server has a few pre-defined "routes", which define the URL paths that the server will respond to. These are: the root path (/), which will display a "Login with Webex" button, the /login path which will perform [2] the redirect to the Webex OAuth Authorization URL, the /redirect path which will receive the redirect initiate from Webex in [4] along with the subsequent access token retrieval in [5]. The /create_meeting path will create a new meeting in Webex using the access token obtained.
Now that your temporary web server is running, you can proceed to the next step to obtain the access token.
As you saw, the root web page on the web server you just created presents a simple "Login with Webex". This button redirects you to the /login route which you will implement next. This route needs to redirect the user to the Webex OAuth Authorization URL that you received when you created the Integration. To do this, we will construct the URL and then use the imported Flask redirect function to redirect the user to the URL.
Open a new browser tab to http://dev1.pod22.col.lab:5002 and click the Login with Webex button.
To create a new meeting, you need to use the Create a Meeting API. The wxc_sdk has a MeetingsAPI submodule that provides access to the various meetings APIs.
The Webex Documentation for the Meetings API object is located at: The wxc_sdk Documentation for the MeetingsApi submodule is located at: The Create a Meeting API is one of the more complex APIs because of the number of parameters it accepts. Most of the parameters are optional, but they provide a great amount of flexibility in how you can create a meeting if you need it. If you scroll down in the wxc_sdk link for the MeetingsApi above, you will find the create() method. As you can see, this method takes a many parameters:
The required parameters to create a meeting are the title , start , and end parameters so in this example, you will create a simple meeting with just these parameters. The easier way to understand all the parameters required is through the Create a Meeting API documentation. Follow these steps to create a meeting:
The API imposes certain restrictions on the start and end times. The documentation states that start cannot be before the current date and time or after end . Duration between start and end cannot be shorter than 10 minutes or longer than 23 hours 59 minutes.
To create a meeting that starts 5 minutes from the time the script is run and ends 30 minutes later, we will use the datetime module to generate the timestamps. Add the following code to the script to create the start and end time variables as well as a variable for the meeting title:
Copy and paste the following code to the script to invoke the Join a Meeting API, generate the join links, and return the output to the user in the form of a web page with links:
If necessary, click Log in at top right and sign in with the following credentials:
Now that you've worked with Calling and Meetings APIs, you can move on to messaging APIs.
Now that you have explored Calling and Meetings APIs, let's take a look at the Messaging APIs. You will use these APIs to create a mechanism by which you can send notifications to a space when some action occurs. This can be useful for notifying groups of people when some event occurs. Such an event can be an outage event or just a notification of a configuration change. For this type of application, it makes sense to use a Bot whose purpose is to send these notifications. This allows the Bot account and its access level to be limited to just this purpose. Because Bots have their own identity, users will see notifications coming from a specific Bot user as opposed to a personal account. You will create the Bot and get its access token in order to send messages on its behalf, then create a Webex Space and add the Bot to it.
In this section, you will perform the following tasks:
You will start by creating a notifications Bot.
The next step is to add the Bot to a Space that will be used for the notifications. Note that this is only one possible way of performing notifications. You could write the bot so it has a list of users to notify and the Bot can send 1:1 notifications to each individual user. You could have also had the bot create this space automatically, but then you would need to create a mechansim to get the bot to add users to that space. For the sake of simplicity in this lab, you will create the space manually and have the Bot search for this space and post to it.
Now that you have a Space with your Bot as a member, you can have the Bot send a message to it. You will use the API by way of the wxc_sdk in a sample script to accomplish this task.
Add the following line to instantiate the WebexSimpleApi object as "api" with this access token.
Add the following to your Python script:
This code calls the list() method of the RoomsApi object, filtered by type='group'. It then iterates through the list of rooms and prints out the JSON representation of each room.
Now that you are able to send messages using a Bot, the next section will cover receiving messages and events.
Until now, your interactions with Webex APIs have involved sending instructions to Webex to perform actions, and receiving success or failure responses. What if you want your application to be notified when a specific event occurs within Webex, such as a message being sent to your bot? Webex utilizes webhooks for this purpose. A webhook is a mechanism for Webex to initiate an HTTP/HTTPS message to a pre-configured URL to notify your application that a specific event has occurred. Although we are going to explore webhooks in the context of Messaging APIs, webhooks can be used for other purposes like receiving events from Webex about Meetings or Calling.
A webhook involves the Webex Cloud initiating an outbound HTTPS request to your applicaiton. Since this traffic is initiated from the Internet (i.e. Webex Cloud), the additional security exposure must be considered. In most cases, a reverse proxy (such as nginx) is deployed on the Internet that forwards requests to your internal application in order to safeguard your internal servers. When Webex sends a message to this reverse proxy, the proxy decides if the request can be trusted and forwards it to an internal server, and that server's reply is sent back via the proxy. Webex also provides some shared secrets in the messages as an additional layer of protection to allow your application to authenticate that the requests were legitimately received from Webex.
In this lab, the reverse proxy portion won't be covered in detail, however you will have the ability to receive events from Webex, which you will need to process. Let's see what this looks like. In this section, you will examine the following key aspects:
If a bot has been configured with a webhook to receive notifications when a message it sent to the bot, Webex sends a webhook notification immediately upon receiving that message, but what does the webhook actually look like? To illustrate this, we have a pre-configured Bot and web server that simply echos back the webhook data that it receives. This diagram shows how this Bot works. When you send a message to this Bot through the Webex App, Webex sends a webhook message as an HTTP POST to the Bot's configured webhook URL. The bot is configured to read the contents of that webhook notification and just echo it back to the person that sent it.
In subsequent sections of this lab, you will configure your own web server to receive and process webhook events in Python. Before creating your own webhook, it is helpful to understand the structure of the message sent from Webex to a webhook URL.
If you take the id value in the data from the response and place it in the messageId field of the Get Message Details , while logged in as pod22wbxuser@collab-api.com (with password: C1sco.123 ), you should see the message as show below in the text field.
Depending on your requirements, you can have a Bot that interacts with users purely using text-based messages. However, Webex also supports adaptive cards, which are a standard way to present a graphical response with features like buttons, check boxes, various input methods for text, numbers, and date/time elements. They are basically text-based content with JSON formatting that will be rendered by the Webex client as graphics and form items.
What this means is that the web server that handles webhook messages from Webex can reply with more than just text. It can also send an adaptive card, a form of JSON-formatted text, that the Webex client can use to display something graphical. It also means that when the user interacts with this interface, say they click a submit button, a different kind of webhook is triggered. Instead of a message , this will be an attachmentActions type of event. Therefore, on your web server, you may choose to send this kind of data and handle the events.
While this lab won't go into details on designing adaptive cards themselves, there are great resources from the Webex Adaptive Card Designer to the Microsoft Adaptive Card Designer (for the latter, make sure you select the correct Webex Adaptive Card version, currently 1.3). These tools provide a way to construct your card, and then simply copy out the JSON.
Now that you have explored the data that is sent to a webhook, you can create a webhook for your Bot to receive these messages. Follow the steps below to create a webhook for your Bot.
Parameter | Value | Description |
name | Cisco Live LTRCOL-2574 Webhook | Not visible, so only useful for admins to determine the use of a certain webhook |
targetUrl | http://collab-api-webhook.ciscolive.com:9022/api/v1/wbxt/events | The "callback" location where Webex will POST webhook events to |
resource | messages | Describes the type of event, either messages or attachmentActions for adaptive cards actions, or a number of other events. Note that you can only ever supply one. To receive webhook notifications for different kinds of events you must create a webhook for each event type. |
event | created | This is a kind of event action. created means any new Message or Attachment Action |
Simply scroll up and paste attachmentActions in the resource line (overwriting the previous information in that row).
You've learned about different API types in the Cisco Collaboration portfolio and how to build queries to achieve common tasks using both tools and in Python with the help of SDKs. The next section will expand on this knowledge through a provisioning web portal example, covering error handling and Python interactions with webhook messages.
Up to this point, you have used tools like SoapUI, explored the Webex documentation, and completed various Python examples to interact with and understand several of the Cisco collaboration APIs. The final goal of this lab is to create a web portal that allows administrators to perform business functions using a simple interface. The goal of this section is to give you an appreciation for the kinds of things you can accomplish using the APIs you have learned about in this lab. This is by no means intended to show you how to create a full-featured and production-ready web portal, but it will give you a taste of what is possible.
If you put yourself in the shoes of a web developer responsible for building the interface for such a portal, you don't want to get bogged down in the details of SOAP, REST, and the complexities of various product-specific APIs. Instead, you ideally want a way to indicate the intent of the user of the web page and let someone else deal with the complexity. For example, a web developer might want to add a button to a web page that allows an administrator to migrate a user from UCM to Webex Calling.
Modern web development usually involves two groups of developers: front-end developers and back-end developers. Front-end developers typically focus on the user interface (UI), including HTML, CSS, and Javascript code that runs in the browser. Back-end developers, on the other hand, focus on the code running on the web server, databases and other data stores, and managing the business logic when a user interacts with the UI. In this lab, you will play the role of the back-end developer, building the framework that allows the web UI to request data for display and to invoke requests for specific actions, such as adding a new Phone to UCM or enabling a user for calling features in Webex.
In the upcoming sections, you will use Python code to extend a web server application to add support for Cisco UCM SOAP and Webex REST APIs. Your goal is to create a web server that can receive a request to perform an action and then invoke the appropriate API calls to perform that action, similar to those you performed using Python in previous sections.
Many modern websites use an API-based approach to development, where the front-end invokes a REST API provided by the back-end. In simple terms, when a user on a web page performs an action, such as pressing a button or submitting some data, the front-end Javascript code makes a REST API call to the back-end with the information that was submitted. The same is true if the front-end needs to retrieve information, for example, getting the list of phones associated with a user. The front-end would make an API call asking for the list, and the back-end would handle invoking whatever API calls are needed to retrieve the data. Typically, the front-end API is simple, indicating an action the user wants to perform. The business logic and complex API interactions are left to the back-end code. For example, when migrating a user from UCM calling to Webex calling, the front end may only need to provide the user ID of the user to migrate and the back-end code would handle the details of invoking the UCM and Webex APIs to perform the migration, handling any complex logic and error handling. When the back-end completes the task, it would return a response to the front-end to inform the user of the success or failure of the operation.
This lab is not intended to be a class on website development; therefore, the front-end code has been completed for you, and parts of the back-end as well. The lab structure allows you to focus on the actual work of programmatically interacting with the various collaboration APIs. The framework for the front-end API has already been created for you. You will be implementing the actions taken when these front-end API calls are received by your server. For example, when the front-end wants the list of active services or some specific performance counters, you will write the code to retrieve those counters and pass them back to the front-end code running in the web browser. The goal is to build capabilities that make it easy for a front-end web developer to invoke business functions, such as adding/removing user phones, provisioning users in Webex, or sending Webex notifications with minimal effort. For example, sending a notification to a Webex space requires values like the RoomID. When implementing a "send notification" function, the back-end can allow the user to just enter the name of the space and handle figuring out the RoomID and other API parameters needed to send the notification.
The diagram on the right shows the overall architecture of the web server. The web server you will develop this portal with will be a Flask application. Flask is a lightweight, extensible web application framework written in Python that allows you to create a web server. One of the extensions that you will be using in this lab is called the Flask-RESTX extension. This extension makes it easy to create a REST API of your own that will be used by the front-end code running in the web browser, similar to the capabilities in the Webex API documentation. This is what we have been referring to as the front-end API.
We have already defined the API endpoints you will be creating. We have also added documentation for the API that you will implement. This combination of the RESTX extension and the documentation in the code enable an interface called the SwaggerUI . These are web pages that are automatically created for you which allow you to easily test your newly created API from a web browser, similar to the way you used the Webex API documentation pages to test the Webex APIs. The built-in SwaggerUI interface gives you a simple web interface to test the front-end API and allows you to make sure it is working without having to write any of the front-end code. Basically you want to make sure the API works before you hand it over to the front-end developer to develop the GUI. As you add more code and documentation to the code, the SwaggerUI picks up those changes and generates new web pages you can use to test the API you built.
Once you have finished implementing the front-end API, you will explore how the front-end "Provisioning Portal" web page makes use of the API and invokes the Python code you have written to give you a good idea of what you could build using these APIs.
One important point to note is that in this lab, the API you are implementing does not require any form of authentication. In a production environment, you would want to create a user login mechanism and then use that to authenticate the API calls from the front-end to the back-end. This is outside the scope of this lab, so the portal you build will not authenticate any of the API calls.
* Running on http://10.0.122.40:5000/ (Press CTRL+C to quit)
Your web portal is active! It doesn't do much yet, but that will soon follow. Let's get started with the CUCM SOAP API functions.
As discussed earlier, the Unified CM provisioning and serviceability APIs are SOAP based. To build the web portal, you will leverage the same Python modules you used previously in this lab in addition to some additional modules to implement the logic for your front-end APIs.
In this section, you will review the following:
We are providing two Python modules, flaskr/cucm/v1/axltoolkit/__init__.py and flaskr/cucm/v1/cucm.py , in order to provide a common core framework allowing the portal server to make use of SOAP APIs. These modules use the Zeep Python library, which you have previously used in exercises. You may open these files as a reference via your VS Code Instance browser tab, using the Explorer . You will utilize several classes & methods from these modules in order to add functionality to your Web Portal. If you don't know what Python modules, classes, or methods are, don't worry. While understanding these concepts would help to understand the underlying code that we have provided for you, it is not necessary to be able to complete this lab.
Not all of classes or methods provided in these pre-built modules will be utilized in the web portal but we are providing them to you for future use.
The following describes in more detail the capabilities provided by these two modules.
These classes are designed to be generic so that you can take them and use them for your own projects if you wish. The Reference section of this lab has pointers to all of this source code that you can make use of. Note that this code is being provided as-is and it is up to you to ensure it provides the level of security and functionality you might need in your environment.
In order for the portal to connect to your Unified CM cluster, you must supply host names, HTTPS port number, and username and password for authentication. We have pre-configured a file that will be used to populate environment variables which will be read by various portal APIs that you will be implementing.
# Environment variables
# Participant pod number
POD_NUM='22'
# DEFAULT CUCM ACCESS INFO
CUCM_HOSTNAME=cucm1a.pod22.col.lab
CUCM_PORT=8443
CUCM_USERNAME=admin
CUCM_PASSWORD=C1sco.123
# DEFAULT CUC ACCESS INFO
CUC_HOSTNAME=cuc1a.pod22.col.lab
CUC_PORT=8443
CUC_USERNAME=admin
CUC_PASSWORD=C1sco.123
# DEFAULT CMS ACCESS INFO
CMS_HOSTNAME=cms1a.pod22.col.lab
CMS_PORT=8443
CMS_USERNAME=admin
CMS_PASSWORD=C1sco.123
# Service App settings (client_id, client_secret, and refresh_token)
In order to utilize the core CUCM SOAP API Python classes you need to instantiate them (AXL, PAWS, SXML) by providing the details of your CUCM server stored in environment variables which came from the .env file you just looked at. You will need to make use of the CUCM_HOSTNAME , CUCM_PORT , CUCM_USERNAME , and CUCM_PASSWORD variables.
When communicating with CUCM and other VOS-based UC applications (CUC, IM&P, etc) over web services based APIs (AXL, PAWS, SXML, REST), you must maintain a web connectivity session where the application can reuse the already established TLS connection as well as cache and re-use the JSESSIONID / JSESSIONIDSSO cookie(s) returned after a successful authentication. If you do not cache these authentication cookie(s) and do not send one with subsequent requests, each API request your provisioning portal makes will open a new session with the product. This may lead to large numbers of active sessions, which, in turn, can lead to overall system performance degradation. Although the Cisco Tomcat (the web server used on CUCM and other VOS-based products) web application sessions have a default timeout of 30 minutes, if the JSESSIONID & JSESSIONIDSSO cookies are not cached by the client and the request rate is high, Cisco Tomcat services will start to spike the CPU and deplete available memory, potentially leading to more serious issues on the server.
You can monitor Active Sessions on a per web application basis by monitoring the following CUCM performance counter:
\\cucm-host\Cisco Tomcat Web Application(axl)\SessionsActive
Performance degradation is also dependent on the request and response activity, but as a general rule active session count over a 100 is of concern. The Using JSESSIONIDSSO to improve performance page on developer.cisco.com has more information on this topic.
In Python you can easily achieve this persistent authentication cookie caching by utilizing the session object available within the Python Requests library https://requests.readthedocs.io/en/latest/user/advanced/#session-objects . The Python requests library is already utilized by the AxlToolkit module in the same way you did in the earlier exercises.
In order to maintain this persistent session object, you must instantiate the CUCM SOAP API Python classes globally in the flaskr/api/v1/cucm.py file. Follow these steps to create new variables and create new instances of the classes discussed earlier.
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')
###########################################
This code creates five variables, one for each of the SOAP API classes. You will later be able to call these to issue commands through these APIs. Notice that the username, password, and hostname information passed into these classes comes from the variables declared immediately before the lines you pasted, retrieved from the environment variables using the Python getenv function. The strings passed into getenv directly map to the names defined in the .env file.
Any time you save a file that is used by your Flask app, you must restart it for the changes to take effect.
These are the APIs being exposed by the backend of your provisioning portal.
Each of the lines you see on this page is an API endpoint we have pre-configured for you. As mentioned previously, this page is generated automatically by SwaggerUI. To see where this all comes from, you can look at the flaskr/api/v1/__init__.py file. That file contains the following code:
from flaskr.api.v1.cucm import api as cucm
from flaskr.api.v1.cuc import api as cuc
from flaskr.api.v1.cms import api as cms
from flaskr.api.v1.wbxt import api as wbxt
from flaskr.api.v1.wbxc import api as wbxc
from flaskr.api.v1.core import api as core
v1_blueprint = Blueprint('api_v1', __name__,
template_folder='templates',
static_folder='static')
v1api = Api(v1_blueprint, title="Lab Portal APIs", version='1.0')
v1api.add_namespace(cucm)
v1api.add_namespace(cuc)
v1api.add_namespace(cms)
v1api.add_namespace(wbxt)
v1api.add_namespace(wbxc)
v1api.add_namespace(core)
The first part of the code with the import statements is importing the other Python files that are in the same directory as the __init__.py file. The content of those files is then added as a separate namespace to the API with the names provided. In this example, the cucm APIs are imported from the flaskr/api/v1/cucm.py file and added as a namespace to the API with the name cucm .
If you look at the flaskr/api/v1/cucm.py file, you will see lines like this example from the file:
@api.route("/line/")
@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):
The first line that starts with @api.route defines a new API endpoint. Because it's in the cucm.py file which was imported as the cucm namespace of the API, this endpoint is prefixed with /api/v1/cucm/, so this line defines the endpoint named /api/v1/cucm/line/ followed by a parameter containing the directory number field as a string.
The line @api.expect(cucm_update_line_query_args, validate=True) tells the API what kinds of parameters to expect. In this case, it says to expect the parameters defined in cucm_update_line_query_args which is defined in the file flaskr/api/v1/parsers.py . If you look in that file you will see this definition:
# CUCM Update Line Query arguments
cucm_update_line_query_args = reqparse.RequestParser()
cucm_update_line_query_args.add_argument('callforwardtovm', type=inputs.boolean, required=False,
help='Enable Call Forward to Voicemail', default=True, location='args')
You can see how this line indicates the name of the argument, the type (boolean), whether it's required or optional, the help text which is displayed in SwaggerUI, and the default value which is needed if the argument is optional.
Finally, in the cucm.py example above you can see there is a function named put that tells the API that what follows is the code to execute if this API endpoint receives a request with the PUT verb. You will see this structure throughout the cucm.py file as well as the other files you will explore in the next few sections of this lab. These functions that define the portal / front-end API are all created for you which is why the SwaggerUI page is populated, but right now, most of the functions that define what the API actually does are empty, so the next few sections.
If you click on the PUT /cucm/line/{directory_num} option on the Swagger UI page you will see this which is rendered from all the code you just explored above:
@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
https://developer.cisco.com/site/paws/documents/api-reference/
"""
try:
version_info = myPAWSVersionService.get_version()
except Exception as e:
result = {'success': False, 'message': str(e)}
return jsonify(result)
apiresult = {'success': True, 'message': "CUCM Active Version Retrieved Successfully", 'version': version_info['version']}
return jsonify(apiresult)
The first line, @api.route("/version") , automatically ensures that a function in this class is called when your web server receives a request on the "/version" URI depending on the verb that was used in the HTTP request. In this case you sent a GET, so the get(self) method is the one that gets called. The meat of what this method is doing is calling myPAWSVersionService.get_version() and parsing the output.
So what does myPAWSVersionService.get_version() do?
myPAWSVersionService is an instance of the PAWS class that you had instantiated at the beginning of this page. The PAWS class has a method called get_version that uses the PAWS API to pull the version information from CUCM. To explore what is happening in the PAWS class, you can see the following function in the flaskr/cucm/v1/cucm.py file:
@Decorators.paws_result_check
@Decorators.paws_setup
def get_version(self, Version='Active'):
if (Version == 'Active'):
return self.pawsclient.get_active_version()
elif (Version == 'Inactive'):
return self.pawsclient.get_inactive_version()
else:
raise Exception("get_version only accepts 'Active' or 'Inactive'")
By default, the function returns the Active version and calls self.pawsclient.get_active_version() to obtain the version. So now what does get_active_version() do? This function comes from the AxlToolkit that has the following method:
def get_active_version(self):
active_version = self.service.getActiveVersion()
return active_version
The get_active_version() method calls self.service.getActiveVersion() . The getActiveVersion() method is one that was automatically generated by Zeep by reading the PAWS API WSDL file. It is this function that takes care of creating a SOAP request and sending it to the server, parsing the response, and returning back the version information from the server.
Finishing up by going back to the first class you looked at above - cucm_get_version_api :
try:
version_info = myPAWSVersionService.get_version()
except Exception as e:
result = {'success': False, 'message': str(e)}
return jsonify(result)
apiresult = {'success': True, 'message': "CUCM Active Version Retrieved Successfully", 'version': version_info['version']}
return jsonify(apiresult)
The last operation to explain here is return jsonify(apiresult) line. You may have noticed why we are using jsonify instead of just returning the apiresult which is a dictionary. The reason is that the jsonify function is useful in Flask apps because it automatically sets the correct response headers and content type for JSON responses, and allows you to easily return JSON-formatted data.
Notice that at no point in any of this code did you need to worry about the actual SOAP request being made. There is no XML to write, no SOAP envelope to construct, no HTTP headers to set. All of that is handled by the Zeep library and the classes you have instantiated. You will find that the majority of the code you need to write is to handle the data that comes back from the the APIs and extract the information you need.
For the rest of this lab, you will be focused on these final pieces where you implement the actual work to make the API calls and parse the results. In this case, getting the version was relatively straightforward, but as you will see, more complex interactions will require more code.
You have just executed your first API call via your portal which called the CUCM PAWS API Version Service (getActiveVersion) request. The code to invoke this API was already provided, but you will be implementing several others on your own. Let's dive a little deeper and implement API functions that will provision your CUCM via the AXL API.
Up to this point, you have checked the portal's connectivity to the pod devices and initialized the global CUCM SOAP API Python classes shown below:
In this section, you will implement the following CUCM AXL Provisioning API tasks using the above mentioned classes:
One thing that you might want your portal to be able to do is display information about a phone configured in CUCM. To do this, you need to provide an API endpoint that can go retrieve the desired phone information from CUCM and provide it to the front-end.
With the help of the CUCM SOAP API Python class myAXL you will send a getPhone thick AXL method and relay the response if successful.
The get() function of the cucm_phone_api class gets called when the portal API receives a GET request to the /phone/<string:device_name> endpoint. All it does is call the get_phone() method of the myAXL class while passing the device_name variable as an argument. The device_name variable is expected to be passed in to the portal API route as part of the URL. For example: "/cucm/phone/CSFPOD22USER1". This type of parameter is also referred as a path parameter . The get_phone() method returns a dictionary containing the phone device configuration details and some error checking is performed before returning the response.
Next you will implement a function to provide the front-end with the ability to add a phone. You will build upon the cucm_phone_api() class where you added a get method earlier.
You may recall from the REST APIs chapter, RESTful APIs map the HTTP verbs GET , POST , PUT , and DELETE to retrieve, create, modify, and delete an object that is being interacted with. In the Portal API, you will be building the POST method for the /cucm/phone/{device_name} portal API route, which will in turn execute a series of CUCM AXL API methods in order to achieve the task of adding the device based on your desired provisioning criteria. This is an example where the logic of the portal API is doing more than just calling an equivalent API on product APIs. This code is orchestrating multiple API calls to achieve a desired outcome of not only adding a phone, but also associating it with a user and setting the primary extension.
The simple business logic is as follows:
With the help of the CUCM SOAP API Python class myAXL you will utilize the following thick AXL methods:
and relay a final response if all AXL API requests are successful.
This is a large block of code, so let's try to step through a few of the lines of code above to understand what is happening. You should first of all understand that this code is being executed any time the portal API receives a POST request to the /phone/<string:device_name> endpoint. The first line you added populates a variable cucm_add_phone_query_parsed_args with the parameters that were passed to your code via the front-end API. The Flask-RESTPlus extension looks at the rules that are defined in the parsers.py file we mentioned earlier and validates that the parameters are present and of the correct type. The second highlighted line of code (line 17) shows the call to the get_user function. This is the function call that issues an AXL request to UCM and retrieves the user data. The next few lines are just parsing through the data that was returned to extract the phone number and associated devices.
Next, you see a dictionary (set of key-value pairs) is created with all the information needed to describe a new phone to add. While this looks like a lot of data, it just represents the required parameters for the phone we are trying to add. There are actually many other optional parameters you can pass that are documented in the AXL API documentation.
After this, you see a call to the add_phone function on line 57 which issues the AXL API call to add the phone. Finally, the last highlighted line shows the AXL API call to update_user which performs the task of updating the list of associated devices for the user.
Next let's work on updating an existing phone's configuration in the CUCM database via the portal API. You will build upon the cucm_phone_api() class where you have added the GET (retrieve) and POST (create) methods earlier. Now you will add the PUT method that will modify a given phone device name.
The simple business logic is as follows:
With the help of the CUCM SOAP API Python class myAXL you will utilize the following thick AXL methods:
and relay a final response if all AXL API requests are successful.
You can see that this code is fairly straightforward. There is a variable created called phone_update_data that contains the data to be updated. In the name of simplicity, the update method you are creating here only allows you to update the calling search space and description of the device. This data is passed to the update_phone method which invokes the AXL API to update the phone. After updating the phone, the apply_phone method is called which tells CUCM to tell the phone to pick up the updated configuration.
The final phone operation you will implent allows you to delete an existing phone device from the CUCM database via the Portal API. You will add the DELETE method to the cucm_phone_api() class that delete a given phone device based on the name provided.
With the help of the CUCM SOAP API Python class myAXL you will utilize the following thick AXL Method:
and relay a final response if all AXL API requests are successful.
You can see that deleting an existing phone is easy. The one line you have added invokes the AXL API call to delete the phone, given the name of the device provided.
Next up let's work on retrieving an existing CUCM end user configuration detail from the CUCM database.
With the help of the CUCM SOAP API Python class myAXL you will utilize the following Thick AXL Method:
and relay a final response if all AXL API requests are successful.
You have added a lot of new capabilities to the Provisioning Portal's APIs. Now it is time to make sure it all works as expected, again utilizing the Portal's RESTPlus/Swagger UI page.
{ "message": "Phone Data Retrieved Successfully", "phone_data": { "AllowPresentationSharingUsingBfcp": null, "aarNeighborhoodName": { "_value_1": null, "uuid": null }, "activationIDStatus": null, ... "automatedAlternateRoutingCssName": { "_value_1": null, "uuid": null }, ... "description": "Cisco Live LTRCOL-2574 - pod22ucmuser", ... "name": "CSFPOD22UCMUSER", ... } }
{ "message": "Phone Added Successfully", "phone_uuid": "{34103D82-4947-7DB7-E2BC-CB2043EEEF74}", "success": true, "user_uuid": "{9E35D561-735E-2357-C4EE-18BBBDA8CB5B}" }
{ "message": "Phone Configuration Updated & Applied Successfully", "success": true, "uuid": "{34103D82-4947-7DB7-E2BC-CB2043EEEF74}" }
{ "message": "Phone Successfully Deleted", "success": true, "uuid": "{34103D82-4947-7DB7-E2BC-CB2043EEEF74}" }
{ "message": "Phone Successfully Deleted", "success": true, "uuid": "{34103D82-4947-7DB7-R2D2-WOK043EFFF77}" }
{ "message": "User Data Retrieved Successfully", "success": true, "user_data": { "associatedCapfProfiles": null, "associatedDevices": null, "associatedGroups": { "userGroup": [ { "name": "Standard CTI Enabled", "userRoles": { "userRole": [ "Standard CTI Enabled" ] } }, { "name": "Standard CCM End Users", "userRoles": { "userRole": [ "Standard CCM End Users", "Standard CCMUSER Administration" ] } } ] } ... "userid": "pod22ucmuser", ... } }
Congratulations! You have added some core CUCM provisioning capabilities to the Portal APIs that allows you to manage phone devices. Let's add some CUCM Serviceability capabilities to the Portal APIs that can help you monitor CUCM.
In this section, you will implement the following CUCM Serviceability XML API tasks in your Provisioning Portal API:
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.
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 .
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.
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 .
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:
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.
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.
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.
{ "TotalDevicesFound": 1, "message": "Device Search Results Retrieved Successfully", "ris_search_result": { "SelectCmDeviceResult": { "CmNodes": { "item": [ { "CmDevices": { "item": [ { "ActiveLoadID": "Webex_for_Windows-45.1.0.31549", ... "Description": "Cisco Live LTRCOL-2574 - pod22ucmuser", "DeviceClass": "Phone", "DirNumber": "\+19197220001-UnRegistered", "DownloadFailureReason": null, "DownloadServer": null, "DownloadStatus": "Unknown", ... "Httpd": "No", "IPAddress": { "item": [ { "Attribute": "Unknown", "IP": "10.0.122.45", "IPAddrType": "ipv4" } ] }, "InactiveLoadID": "Webex_for_Windows-45.1.0.31549", "IsCtiControllable": true, "LinesStatus": { "item": [ { "DirectoryNumber": "\+19197220001", "Status": "UnRegistered" } ] }, "LoginUserId": "pod22ucmuser", "Model": 503, "Name": "CSFPOD22UCMUSER", "NumOfLines": 1, "PerfMonObject": 2, "Product": 390, "Protocol": "SIP", "RegistrationAttempts": 0, "Status": "UnRegistered", "StatusReason": 0, "TimeStamp": 1654799252 } ] }, "Name": "cucm1a.pod22.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.
{ "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 }
{ "message": "PerfMon Data Retrieved Successfully", "perfmon_class_result": [ { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\Allocated FDs" }, "Value": 7712 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\Being Used FDs" }, "Value": 7712 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\Freed FDs" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOAwait" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOCpuUtil" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOKBytesReadPerSecond" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOKBytesWrittenPerSecond" }, "Value": 229 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOPerSecond" }, "Value": 24 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOReadReqMergedPerSecond" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOReadReqPerSecond" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOReqQueueSizeAvg" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOSectorsReadPerSecond" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOSectorsReqSizeAvg" }, "Value": 18 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOSectorsWrittenPerSecond" }, "Value": 458 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOServiceTime" }, "Value": 0 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOWriteReqMergedPerSecond" }, "Value": 8 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\IOWriteReqPerSecond" }, "Value": 24 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\Max FDs" }, "Value": 582286 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\Total CPU Time" }, "Value": 109266374 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\Total Processes" }, "Value": 207 }, { "CStatus": 1, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\System\\Total Threads" }, "Value": 1138 } ], "perfmon_counters_result": [ { "CStatus": 0, "Name": { "_value_1": "\\\\cucm1a.pod22.col.lab\\Cisco CallManager\\RegisteredOtherStationDevices" }, "Value": 2 }, { "CStatus": 0, "Name": { "_value_1": "\\\\cucm1a.pod22.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.
The next few sections will take you through the steps implement a variety of capabilities allowing your portal to interact with Webex APIs. To achieve these goals, you will start with the following steps:
In the example you completed earlier, you obtained an access token using a Service App client ID , client secret , and refresh token . This enabled administrative access to Webex. In this section, you will use the information you obtained previously to enable your Portal to perform administrative tasks in Webex as well.
Source Variable ( examples\service_app.py ) | Destination Setting ( .env ) |
---|---|
client_id=' Ca083181a235caf__Your_Client_ID__caff336f93cd1ded ' | SERVICE_APP_CLIENT_ID=' ___PASTE_SERVICE_APP_CLIENT_ID___ ' |
client_secret=' 083181aCa083181a__Your_Client_Secret__1a23181a2ca ' | SERVICE_APP_CLIENT_SECRET=' ___PASTE_SERVICE_APP_CLIENT_SECRET___ ' |
refresh_token=' Q4ZG1NmIa__Your_Refresh_Token__-733-48f-86f-be4d ' | SERVICE_APP_REFRESH_TOKEN=' ___PASTE_SERVICE_APP_REFRESH_TOKEN___ ' |
Now you can start using the wxc_sdk in your project.
To handle this automatically, we have created a class:
In this code, self.get_tokens() will either read a set of access/refresh tokens from a file, or use the environment variables you set above. If an access token is about to expire or isn't found to begin with, then the refresh token will be used to create a new access token, as you did in the example. The access token is then used to create an instance of the API. By placing this token refresh capability in this class, each time a new instance of this ServiceApp class is instantiated, the token may be refreshed, if needed, as long as the refresh token has not expired.
To create an instance of the ServiceApp class, you just need this single line of code:
In previous examples, you had created a variable named api which contained an instance of the WebexSimpleApi class and used it to perform API calls like this:
You will be using the sa instance of the ServiceApp class to perform the same API calls. The ServiceApp() class calls the WebexSimpleApi class for you but before doing so, it will check if the access token is still valid and refresh it if needed. You can perform the same people list call like this:
If you would like to explore in more detail how the ServiceApp() class works, you can look at the code in the flaskr/api/v1/service_app/__init__.py file.
Now you are ready to add to the portal code. Remove the pass from the section in get_person_det and replace with the following:
The get_person_det() function takes an email address as an argument and returns the detailed user information for a user with that email address. This code is very similar to what you did in the previous examples. It creates a new instance of the ServiceApp class, then uses it to look up a user using the people.list() API and then retrieves the detailed user information using the people.details() API.
Now if your Portal API receives a request on the /user/<userid> endpoint, it will look up that user in Webex and return the result as JSON. If the user is not found, it will return an error message.
The put accespt a single argument, the userid which is the user's email address. The first step to enabling a user for Webex Calling is to get the detailed user information by calling get_person_det() method that you just implemented, then look up the Webex Calling Professional and On-Prem UCM licenses. Since this is also a common task, a get_lic_by_name() function was created to look up the respective license in the same way you did in the earlier example sections. Next, query the Location to which this user and number will be assigned. Following this, look up the user's phone number, which should already be pre-populated based on what was synchronized to the account from the Active Directory via the Directory Connector. If no number is found for the user, return an error. This is an area that could require a lot of custom enhancements. For example, you could use APIs to look for available numbers in the location and assign the next available number to the user.
Add the following to your code:
Add the following to your code and then examine it. The comments in the code explain what each step does.
The put method in the wbxc_user_disable_api class has already beeen implemented for you. This function is used to disable Webex Calling and just does the opposite of the steps you just perfromed. The function looks up the user details and the license, exactly as before, then removes the Webex Calling License and adds the Unified Communication Manager (UCM) license. The only other difference is that since removing the Webex Calling license also removes the phone number, you can simply save that data before updating the user's license and then running another update to the user afterwards to re-apply the phone number. You can look at the code in the wbxc_user_disable_api medthod if you want to see the exact code that does this.
First test retrieving a user.
In a previous section of this lab, you enabled Webex Calling features for the user pod22wbxuser . To test the code you just implemented, you will first disable Webex Calling for this user, then re-enable it.
Now go through the steps to re-enable Webex Calling on the phone using the API you created.
Webex Calling has a vast amount of configuration exposed via the API. From placing calls to configuring call handling, many things can be automated and simplified depending on your requirements. In the next section, you will take a look at using a Bot to send notifications as well as receive commands that can execute some of the same APIs that you have already implemented.
In this section, you will look at add some basic messaging functionality in the portal using the Bot you created earlier.
With the SDK software already installed, implement a send_message() function using the following steps:
Add the following to your code:
This is the same as the earlier example, with the exception that you will search through all Rooms to find one with a particular Room title, and some basic error and exception handling.
Now you can move on to attempting to send a message using the Bot via the send_message API you just implemented.
Now that you can send messages from the portal via the Bot, you can focus on receiving messages via webhooks. In this chapter you will:
This is an area that was not covered in previous examples, because the tasks require a web server, such as the Flask instance you are working on, to receive HTTPS POST messages from Webex. You experimented with a bot that echoed back the webhook messages to get a feel for how they work, but now you will enable your own bot to receive and process webhook notifications from Webex.
The earlier webhook section involved sending requests to create two webhooks, one for messages , one for attachmentActions event types with an HTTP callback destination. Those webhooks will remain active until they are removed or they encounter enough errors to where Webex marks them disabled (for example, after 100 failed attempts in a 5-minute period).
To make sure that you don't have multiple webhooks registered, a good housekeeping practice is to remove any existing webhook and re-add them each time you start your application that makes use of the webhooks.
Your first job is to finish out the functions that remove and re-add the webhooks. Starting with the delete_webhooks_by_name() function. The SDK has a WebehookAPI with some methods that mirror the capabilities from the Webex documentation.
The wbx_bot_api.webhook.list() method is the SDK's version of the Webex List Webhooks API.
As with other list() methods, you can iterate through until you find the "name" that matches the WBX_WEBHOOK_NAME , a constant set to "Cisco Live LTRCOL-2574 Webhook", and then call the wbx_bot_api.webhook.webhook_delete() method supplying the webhook ID of the one you found.
Remove the pass at the beginning of the function and replace it with the code below.
Find the create_webhooks() , remove the pass at the beginning of the function and add the following to your file:
Add the following code to your file:
You may have noticed that the webhook callback URL is http://collab-api-webhook.ciscolive.com:9022/api/v1/wbxt/events . This callback destination routes from Webex directly to your lab Flask web server. The webhooks will be routed to /events in this flaskr/api/v1/wbxt.py file, where this Namespace is configured.
You have webhooks configured and a Flask app to receive them. You just need to decide how to handle the message as it arrives.
Determine if this is a message that you want to respond to
Based on the message reponse, you are clearly not done. Next add an adaptive card to the response.
The intricacies of adaptive card design is definitely beyond the scope of this lab. It is just a JSON-formatted text file used implement a graphical element similar to an HTML web form, including certain graphics, buttons and drop-downs. For this lab a simple adaptive card has been provided in the file: flaskr/api/v1/adaptive_card.json .
It has a few actions , that will generate data that you can see when when a user clicks a button on the card. If you like, you can copy the contents of that file and paste it in a message to the Webhook Echo Bot (webhook-echo@webex.bot) and see what it looks like. Shortly, your web server will be responding with the same content, so you can see it then.
In your code, you need to do two things: send this adaptive card as part of a message, so the sender can see it; and then handle any submissions. Start by sending the card:
Scroll up a bit in the file to find the send_card function. Remove the pass statement and replace it with the following:
So far, you are using the webhook to respond to text-based messages. The reply now also includes an adaptive card. The final step is to respond to submissions by that adaptive card, which triggers a second webhook.
All the work has to be done in the respond_to_button_press() function. Similar to replying to a message, you need to determine the room the message originated from. Instead of a message, an attachment action must be read, which will allow you to read the submitted data as well as determine the sender. You will again only reply to a sender within your own Webex organization.
Congratulations! You have completed have implemented several of the Python functions necessary to create the Portal API that can now be used by the front-end of the Provisioning Portal. The next section will take you through a tour of the portal.
Now that you have implemented the portal API that provides a way for the web front-end to leverage the capabilities made available by the Unified CM and Cisco Webex APIs, it's time to explore the finished product - a working portal that leverages these APIs to provide some monitoring capabilities as well as the ability to easily provision a user's devices and migrate users between UCM Calling and Webex Calling. Previous versions of this lab also included sections on Unity Connection and Cisco Meeting Server APIs, but in the interest of time they have been removed, however the portal for still makes use of these APIs as well and you're free to explore and use this code in your own projects if you so desire. Remember that all the code in this lab is available for you to download from Github as shown in the Reference section. The portions of the API that interact with Unity Connection and CMS are already implemented for you so that those parts of the portal will function.
To access the portal, navigate to http://dev1.pod22.col.lab:5000 . Another tab should open and you should see a dashboard that looks something like this:
If you have implemented all the code in the previous sections of this lab correctly, you should see the cards at the top populate with the counters, the service list should populate, and you should see the running version on your Unified CM along with Unity Connection and Cisco Meeting Server.
This one page indirectly makes use of 5 different APIs! The front-end developer who was tasked with building this dashboard has no idea how many APIs are being used on the back-end, and nor should they care. This web page is making API calls to the API you created which in turn calls the respective APIs on Unified CM, Unity Connection, and CMS. Here is where the data is coming from:
Although the back-end you implemented is written in Python, the front-end code must be written in Javascript, as that is the only language that can be executed by the web browser. To get a better understanding of how the portal is obtaining this information, you will need to look at the Javascript code that is responsible for making the API calls to the back-end. Follow these steps to look at the code. You will not be implementing anything in this section, but rather looking at the Javascript code and understanding how it interacts with the back-end.
The front-end of the portal makes use of common web technologies - HTML, CSS, and Javascript. As mentioned earlier, the goal of this lab is not to teach you how to be a front-end developer, so the code for the front-end is being provided for you. That said, it is still helpful to explore this code and understand how the front-end makes use of the APIs you created.
All the code for the front-end can be found in the flaskr → static and flaskr → templates directories in your project. In the static directory, you will find the following folders:
The templates folder contains all the HTML files for the various web pages. In case you are interested, the HTML templates in the folder make use of Jinja templates which allow for the ability to reuse components of the web pages.
First look at the HTML template that first loads when you navigate to the Dashboard to see which Javascript code it is executing. Navigate to flaskr → templates and open the index.html file. Towards the end of the file, you should see the following. This code gets executed by the browser once the web page has finished loading.
The four lines highlighted above are the calls to the functions defined in the .js files. The focus on this section will be on the Javascript code. Navigate to the flaskr → static → js → portal directory in VS Code. You should see six files - core.js, cms.js, ucm.js, ucxn.js, wbxc.js, and wbxt.js. Each of these contain the Javascript relevant to each product the portal interacts with. Start with the simplest of the functions - the three that retrieve the code version and update the website.
Start with the update_ucm_version() which, as the name implies, retrieves the version of Unified CM. Open the ucm.js file. Navigate to the end of the file and you should see the following:
The two lines highlighted above show the most important parts to understand of this code. The first highlighted line shows the function call get_ucm_version() . You will examine the contents of this function in a moment. The get_ucm_version() function will make the API call to retrieve the version from the back-end. Once the version is retrieved, the code after the then gets executed, extracting the result from the response and updating the HTML element on the web page to show the version.
Now search for get_ucm_version() in the same ucm.js file. You should see the following code:
You can see that this function is very simple. This code makes use of a very popular Javascript library called jQuery . This library provides a function called ajax which allows for the asynchronous execution of HTTP requests. You can see the two parameters passed to this function are very straightforward: the type which is the HTTP verb to use, GET in this case, and the url. Notice that the URL does not contain the hostname. This is because the web browser knows to send the request to the same server that is already serving the web page. Here you see the API call to the /api/v1/cucm/version endpoint that you implemented earlier. That code on the back-end, in turn, makes use of the PAWS API to retrieve the active version from Unified CM. As you can see the front-end developer does not need to know anything about SOAP, PAWS, the UCM IP address, login credentials for the server, etc... This makes it very easy for the front-end developer to obtain the version information and use it however they like on the web portal.
If you look in the ucxn.js and cms.js files. you will find similar functions being called for obtaining the Unity Connection and CMS versions as well. Feel free to explore these on your own.
The next function that was called when the web page loaded was refresh_ucm_perfmon_data() . You can also find this function defined in the ucm.js file. You can see that the first part of this function looks like this:
Basically this function calls get_ucm_perfmon_data() and then uses that data returned to update the various cards displayed on the dashboard screen. To see how this function retrieves the perfmon data, find the get_ucm_perfmon_data() function in the ucm.js file. It should look like this:
Again you can see that the front-end developer has very easy access to the performance counter information. If you wanted to make this even simpler, you could have hard-coded the counters on the back-end and just returned them without the front-end developer having to know about the individual counter names. In this case, the implementation provides some flexibility that allows the front-end developer to control what data to retrieve at the expense of some additional complexity of having to know the names of the counters. This is always a balancing act of balancing ease of use with flexibility.
You can see that in this case, the request is a POST to the /api/v1/cucm/perfmon endpoint which, on the back-end, uses the Serviceability XML API to retrieve the PerfMon counters. The biggest difference between this request and the previous requests to retrieve the version is that it also contains a data field that provides the list of counters to retrieve. For a seasoned Javascript developer, making requests like this are second-nature.
If you want to explore some more on your own, go back to refresh_ucm_perfmon_data() and look at how the counters returned are manipulated to add certain values for display on the web page. You can also explore how get_service_status() is called to retrieve the service states and populate the list of services as well as determining whether to indicate that all services are up or not.
Now that you have explored the dashboard that makes use of the serviceability and monitoring APIs, you can turn your attention to the user provisioning page. On the left side of the portal screen, click on the Users / Devices link. You should see a screen that looks like this:
In the upper right corner of the screen, you should see a box labeled "Enter User...". In this box, enter pod22ucmuser and click the search icon. After a few seconds, you should see the page refresh and show the user profile information that should look similar to the screen below.
This page on the portal is, once again, leveraging several APIs on the back-end to provide the information you see. The user details and associated devices (even though there are no associated devices at this point) come from the AXL API. The AXL API is also what allows you to add a device in the "Add Device" section. Voicemail and the Meeting Space cards on the top of the page make use of the CUPI and CMS APIs respectively. The voicemail button also makes use of the UDS API to look up a user's phone number and display name.
In previous steps in the lab, you had added the CSF device to Unified CM and then deleted it, so the portal should show that no phones are currently associated with the user. The beauty of this portal is that it makes user administration very simple and streamlined. To add a phone, the administrator only needs to enter the device name, model, and a description. No additional input is required. If this were to be deployed in a production environment you would likely have an option to pick a class of service or some other policy to define the Calling Search Space to determine calling permissions for the user, or perhaps you would look up the user's role in the company on the back-end and automatically determine the class of service from their job role instead of worry about this on the front-end web page.
Before exploring the Javascript code behind the user provisioning page, take a moment to use the page to perform some administrative functions of adding a phone, enabling voicemail, and adding a CMS space for the user. First re-add your Webex App device so that it can register again.
The Webex App on your desktop is likely still registered with Webex Calling, so if you would like to test to see if the addition of the phone worked, you can log out of the Webex App and log back in as pod22ucmuser@collab-api.com with password C1sco.123 . Once registered you should be able to dial the voicemail pilot by dialing 5999999 from the Webex App. Feel free to skip this step if you like.
The provisioning portal also provides limited functionality for Webex Calling Users. It could be expanded to include more capabilities, but for the sake of example, the portal allows you to enable and disable Webex Calling for a user or migrate a user from UCM Calling to Webex Calling and vice versa. You should still be on the Users / Devices link of the provisioning portal.
Now search for the Webex Calling user. In the upper right corner of the screen where you had previously searched for the user pod22ucmuser , replace the user name with pod22wbxuser and click the search icon. After a few seconds, you should see the page refresh and show the user profile information that should look similar to the screen below.
This page on the portal is, once again, leveraging several APIs on the back-end to provide the information you see. When the user search occurs, the backend is actually looking for the user in both UCM and Webex Calling. The backend then returns data about whether the user is enabled on UCM, Webex Calling, none, or both. The frontend logic then determines which UI and options to display depending on what kind of user it found. In this case you can see that the user information is limited in this example, but could include more information returned from the API. The portal generates a cross-launch URL using the user ID returned from the API so that you can click on the Configure Calling Settings in Control Hub button which would require appropriate credentials to log in.
The portal provides is the ability to disable or enable Webex Calling or migrate the user back to UCM Calling. If you would like, click the Migrate to UCM Calling button. After a few seconds, the UI should update with their UCM device information and provide you with the option to migrate back to Webex Calling. Note that sometimes the phone number synchronized from the directory gets removed, so if you re-add the user, be sure to enter the number +19197220002 into the phone number field before you re-enable Webex Calling.
Now that you have see the provisioning portal in action, it's time to look at how this all works behind the scenes. You will notice that it is very similar to what you saw for the Dashboard page on the portal.
First take a look at the code that is executed when a user enters a user ID to search for. Open the core.js file and search for the function user_search() . You should see code like this:
Take a look at the lines highlighted above. The first highlighted line is just retrieving the value of the username that the user has entered into the search box on the web page. This value is then passed into the find_user(username) function. Search for the find_user function in ucm.js to examine how it works. You should see the function that looks like this:
Again, the code needed in the browser to interface with the API on the back-end is very simple. It just makes a simple API call using the GET method to the /api/v1/core/user/ endpoint. If this were a production application, you would want to have some authentication for these API calls. This API call searches both UCM and Webex Calling for the user and then returns a response indicating where the user was found and (if applicable) the user details for that user.
The rest of the user_search function is looping through the response and performing actions depending on whether the user is found in UCM, Webex Calling, both, or neither. Feel free to explore the various functions that are called as a result of the logic. For example, show_ucm_user_data located in the ucm.js file.
The next part of the portal code to explore is the code that handles adding a device. You will see that this is also quite simple for the front-end developer. In the ucm.js file, search for the add_ucm_device function.
The add_ucm_device function gets called when someone clicks the "Add Device" button on the provisioning portal. You can see that the first few lines pull the data from the web form where the user entered the device name, description, and model, as well as the name and user ID of the user that was retrieved earlier during the user search. This information is then passed to the insert_device function. If the insert is successful, the portal calls post_wbxt_notification to post a message to Webex indicating that the phone was added.
Search for the insert_device function in ucm.js to see how it works.
Similar to other Javascript functions you have already seen, this function sends an HTTP POST to the /api/v1/cucm/phone/ endpoint and passes the required data. Notice that in this case, the calling search space is being hard-coded into this request. In a production application you would likely want to determine which CSS to configure through some other method. You can see how little work is needed by the front-end developer to add a phone. Also note that in this example, the phone data parameters are being passed as arguments in the URI as opposed to in the message body. This was done as a workaround for a problem we encountered with the swagger UI we are using in the lab, and the better approach would be to send the parameters in JSON format in the message body.
As mentioned earlier, if the phone insertion is successful, a message is posted to Webex by calling the post_wbxt_notification function. Open the wbxt.js file. This should be the only function you see in that file as shown below:
You can see that this function sends an HTTP POST to the back-end with a room name and the message text to send. The back-end takes care of all the details of interfacing with the Webex cloud and sending the message.
You have completed the lab. The following sections are reference sections that give you some additional information about some of the APIs, the tools you have used, as well as some instructions for setting up your own development environment similar to what we have set up for this lab. Thank you for your participation and we hope you forge ahead and leverage APIs in your own environment to automate and simplify the provisioning, monitoring, and operations of your Cisco collaboration environment. Also, as a reminder, the code for this lab is available on Github at the link provided in the references section.
Besides the content you have already discovered in this lab, we have provided some additional sections that let you explore some other tools to work with some of these APIs, as well as add extra functionality to your portal.
Specifically, we will cover the APIs for Cisco Unity Connection and Cisco Meeting Server, each of which have REST-based APIs that are fairly straightforward. For each case, he did not leverage an SDK, as we did with the Webex APIs, but rather built a reusable REST layer for each of these APIs to use.
The bonus section content falls into two categories: product-specific APIs and how to work with them using tools such as Postman, and Python examples implementing portions of these APIs by way of the portal.
While the Webex APIs have a querying tool build directly into their documentations, the Unity Connection and CMS APIs do not. Therefore, we will introduce a popular tool to accomplish the same tasks.
Postman is already installed on your student virtual machine. You will first build your own queries to understand the process better, then use a Postman Collection, which is simply a pre-defined group of queries that can be organized and use variables to avoid having to build every query from scratch.
All REST APIs we will work with use the HTTPS protocol to ensure that the API requests are encrypted. In many cases, the SSL certificates on the servers providing the REST API use self-signed certificates. As a result, clients like Postman may generate an error because they cannot trust the server. In a production environment, you would want to use proper CA-signed certificates so that this is not an issue. For this lab, you will have Postman simply disable SSL certificate verification so that it will ignore untrusted certificates. Perform these steps:
Now you are ready to attempt your first request. For illustration purposes, you will send a request to a Cisco Meeting Server (CMS) to query its system status.
<softwareVersion>3.1.2</softwareVersion>which is nested under <status>...</status>
While you could create each query by hand like this, pasting the URLs and filling in header/content values, Postman has the concept of a Collection , which are simply several preconfigured queries in a folder. You should already have a Collection loaded into your Postman instance, called HOLCOL-2574 . The Collection will have required URLs, pod number and credentials pre-configured for all queries, so they don't need to be entered each time. Follow these steps to explore your Postman Collection.
In the next section you will explore a RESTful API in depth: the Cisco Unity Provisioning Interface API.
Cisco Unity Connection has a set of feature-rich APIs to accomplish various tasks:
Most of these APIs function very similarly. They are all REST-based and can typically respond with either JSON or XML in the response body. As mentioned above, the API for managing and provisioning a Unity Connection system is the CUPI API. Most of the other APIs simply add more functionality for specific use cases and types of server or client data.
NOTE: this lab will focus entirely on features exposed by the supported APIs, which means it is not a direct database query/request. Unity Connection does have some ability to do this using tools such as the Cisco Utilities Data Link for Informix (CUDLI) and/or the Python Scripting Host , however these tools are generally meant to handle specific one-off scenarios and are not part of this lab and should not be used for regular, ongoing automation tasks.
In this section, you will perform the following tasks:
In Unity Connection, users can either be created manually or imported either via LDAP or via a CUCM server using AXL. For this lab, you will use LDAP. For this, the LDAP synchronization between Unity and the LDAP server is already set up. When synchronization occurs, Unity simply learns about those users that exist in LDAP and are available for import, which is to say, have actual accounts with or without a voicemail box created in Unity Connection.
First, generate a query to get the users that can be imported as show in the following steps.
{
"@total": "5",
"ImportUser": [
{
"alias": "Token_User_501dfdf6-2861-4072-a4f8-8abaa0f5b7aa",
"firstName": "",
"lastName": "Token",
"pkid": "8993458a-4a99-47c4-92ec-3f01bfa06942"
},
{
"alias": "pod22user1",
"firstName": "Pod22",
"lastName": "User1",
"pkid": "6ad0a9c9-260a-5663-668f-275a290c8be2"
},
...
Note: The fact that the data under ImportUser is a JSON list (as denoted by the square brackets, [ and ] ) is significant: if @total , that is to say, the number of items under ImportUser , were exactly 1, then the user data under ImportUser would be a JSON dictionary with that user's settings (alias, firstName, lastName, and pkid) and not in a list of dictionaries. When working with these results programmatically as you will do later in the lab, you need to account for both scenarios.
Searching in CUPI for users of any type is done via parameters in the URI. The following steps will show you how to add a query for the alias that matches pod22user1 exactly. The part that would need to be added to the URL is ?query=(alias%20is%20pod22user1) . If you consider that %20 is the hexadecimal representation of the space character, this is very readable (e.g. ?query=(alias is pod1user1) ). Aside from the is keyword, CUPI also supports startswith in order to perform a wildcard search.
{
"@total": "1",
"ImportUser": {
"alias": "pod22user1",
"firstName": "Pod22",
"lastName": "User1",
"pkid": "6ad0a9c9-260a-5663-668f-275a290c8be2"
}
}
Importing a user, just as creating a user in Unity Connection, consists of a POST request and specifying not only what user should be imported, but also a template to use when creating this user. This is exactly the same in the Unity Connection GUI, since there are so many settings, the system only allows you to add a user using a template that has most settings filled in for you. In the actual Postman request, we need to not only specify parameters, as we did earlier, but also add a body to the request.
{
"dtmfAccessId": "12345",
"pkid": "dbc37047-7565-6b29-3327-18850f64d406"
}
/vmrest/users/3967c91f-30ee-4656-83ac-3bc4600ee0dd
This occurs despite that your request still has an Accept header with "application/json". However, since this is the reply to a POST instead of a GET , the response is different and you need to be able to handle this kind of response, as well.
{
"@total": "1",
"User": {
"URI": "/vmrest/users/3967c91f-30ee-4656-83ac-3bc4600ee0dd",
"ObjectId": "3967c91f-30ee-4656-83ac-3bc4600ee0dd",
"FirstName": "Pod22",
"LastName": "User1",
"Alias": "pod22user1",
"City": "",
"Department": "31",
"DisplayName": "Pod22 User1",
...
"ListInDirectory": "true",
...
"DtmfAccessId": "12345",
...
"UserVoicePinURI": "/vmrest/users/3967c91f-30ee-4656-83ac-3bc4600ee0dd/credential/pin",
...
You have also see the ListInDirectory and DisplayName settings, which you will modify next. The PKID for this user account is in the ObjectId field. It is the same ID that was returned when importing/creating the account using the LDAP user. Copy this user PKID (ObjectID) value from your response for the next step
Modifying most user settings is not difficult. It requires the PUT request and information in the Body indicating what settings to change. Not all settings can be modified. Some things, such as the alias, first name / last name, and password are controlled in LDAP because you chose to import that user instead of creating the user from scratch, where everything can be specified and modified.
{
"DisplayName": "Updated User",
"ListInDirectory": "false"
}
{
"@total": "1",
"User": {
"URI": "/vmrest/users/3967c91f-30ee-4656-83ac-3bc4600ee0dd",
...
"DisplayName": "Updated User",
...
"ListInDirectory": "false",
...
Updating a voicemail user's PIN and unlocking the mailbox is one of the most common administrator tasks. PINs can be forgotten, and after repeated failed attempts, a mailbox is locked for some time. Luckily, unlocking the mailbox and changing the PIN is no more difficult than other user setting updates. Note that existing passwords and PINs can never be retrieved, however they can be changed, if they were forgotten. Also be aware that in this lab scenario, the user voicemail accounts are imported from our LDAP directory. User passwords , not the PIN, are utilizing LDAP authentication, so they cannot be changed in Unity Connection (either via the GUI or API). If these were stand-alone users not tied to LDAP, then that would, of course, be possible.
{
"Credentials": "14235834",
"HackCount": 0,
"TimeHacked": []
}
This sets the PIN credential and unlocks the account by resetting the HackCount and TimeHacked settings (the latter being set to a blank value).
{
"errors": {
"code": "DATA_EXCEPTION",
"message": "Password does not have enough characters",
"DetailCode": "19"
}
}
Deleting a user is simple, as expected. It requires a DELETE operation sent to the /vmrest/users/ URL followed by the pkid of the user account to be deleted. Note that this simply deletes the account in Unity Connection. In this case, this same user would then be available to be re-imported via LDAP.
Feel free to close some or all of the requests in Postman at this time (no need to save anything), as you will be adding more in the next section.
Now that you are familiar with Unity Connection's CUPI API, you can dive into the Cisco Meeting Server (CMS) API, another REST-based API.
Cisco Meeting Server (CMS) has a rich REST-based API that allows configuration of nearly all settings, many of which can only be configured via the API, especially when working with clustered CMS systems. Any CMS server can have the WebAdmin service enabled, which also enabled the administrative GUI page. A separate account may be used for API access, or an admin account can be used.
The Cisco Meeting Server API documentation can be found here: https://www.cisco.com/c/en/us/support/conferencing/meeting-server/products-programming-reference-guides-list.html
CMS supports the regular GET , POST , PUT , and DELETE verbs to retrieve, create, modify, and delete an object. All of the requests will be sent to /api/v1 . Responses could include 503, to indicate a busy condition, a 404, if the destination is not found, or other 4XX and 5XX response codes, which will are documented in an XML-encoded reply to the message body.
There are a few features to note:
To illustrate Meeting Server capabilities, you will configure the following using Postman:
You already retrieved the system status of CMS in the REST overview section. You will repeat this using a Postman Collection just to make sure everything is set up.
Creating a Space in CMS is fairly straightforward. A POST request is sent to /api/v1/coSpaces . Note that in the CMS API, Spaces are always called coSpaces, deriving the name from its origins with Acano. In the Header of the request, the Content-type must be specified as application/x-www-form-urlencoded . This will pass the information you enter in the Body section as part of the URL in the correct format. In this case, all of the key/value pairs will be encoded something like this to the URL itself:
name=New%20Space&callId=5555&secondaryuri=5555&uri=spaceSo technically these are URL parameters instead of a payload as we have seen with JSON previously. But having the correct Content-type specified is significant. If the server does or does not expect a particular Content-type may determine the success of the query. As you can already see, even though both Unity Connection's CUPI and the CMS API are both REST-based, they have some significant differences in how the informtion is passed to them.
name:New Space uri:space secondaryuri:5555
/api/v1/coSpaces/2cb3a80a-51a4-4086-9487-fc06087e78edCopy this object id (the coSpaceID) value from your Location header, since you will need to use it in the next step.
Modifying the Space is similar as creating the Space, since you are sending data (with the desired changes), however that the Space ID will now be part of the URL.
Retrieving the list of all Spaces is simply of sending a GET request to /api/v1/coSpaces .
And here you can see the coSpace ID, which was only part of the Location reply header when created. Take note of this since you will need it in the next step.
You'll notice that this request was not very specific, in that it always retrieves the entire list of coSpaces unless
a
filter
parameter is specified, for example,
https://cms1a.pod22.col.lab:8443/api/v1/coSpaces
?filter=pod
The problem that you will need to solve later, is that the filter above,
pod
is searching coSpace names, URIs,
and
secondaryURIs for
*pod*
. So if the filter only contains a single letter, it is almost the same as
getting the entire list. There is no way of specifying a specific user, without using that user's object ID.
A second consideration is that CMS can limit the number of items it returns. Currently the documentation states that the internal limit is 10, meaning if you have a 100-Space system and you send a request to GET /api/v1/coSpaces to retrieve them all, you will receive only 10 coSpaces in the reply, however , the total= value (see the reply above) will indicate the total number of matches (not the number returned). Using this total and the offset parameter, will allow us to get a "page" at a time, if you need to get a larger list of coSpaces. For example, if a GET /api/v1/coSpaces (with offset 0 implied) will return, say, the first 10; a GET /api/v1/coSpaces?offset=10 will retrieve from 11-20, etc.
Do not assume the number returned will be limited to 10. This number may change and has been observed in certain versions to have changed. The point is simply not to assume that if you are requesting all records, that all will be returned at a time.
Deleting a Space simply requires the coSpace ID, which you have from the previous query.
Now you are ready to look at notifications using Webex.
Now that you have had some experience interacting with SOAP-based APIs, you can turn your attention to the other type of web service: REST. In order to send REST-based requests using Python, this lab will utilize the Python Requests library. To make this re-usable, a REST class will be implemented as a basic building block to send and receive the various request types (GET, POST, PUT, and DELETE) and to manage session re-use, so that each REST API, such as CUPI and CMS, can extend the class by adding the API-specific headers and parsing of the responses.
While you won't be able to test this code individually here, all subsequent REST modules will leverage it.
The REST class will use the following parameters:
Within this class, besides the __init__ method to initialize the class, there will be two methods:
Start implementing these methods using Visual Studio Code.
Add the following to the __init__() method:
def __init__(self, host, username=None, password=None, base_url=None, headers={}, port=443, tls_verify=False):
"""
Initialize an object with the host, port, and base_url using the parameters passed in.
"""
self.host = host
self.port = str(port)
self.base_url = base_url
self.session = Session()
self.session.auth = HTTPBasicAuth(username, password)
self.session.verify = tls_verify
self.session.headers.update(headers)
Insert the highlighted line below to assign the url variable:
# Set the URL using the parameters of the object and the api_method requested
# in a format such as: https//host:port/api/v1/api_method
url = "https://{}:{}{}/{}".format(self.host, self.port, self.base_url, api_method)
Insert the following below the # Send the request comment line:
# Send the request and handle RequestException that may occur
try:
if http_method in ['GET', 'POST', 'PUT', 'DELETE']:
if http_method == 'GET':
raw_response = self.session.get(url, params=parameters)
elif http_method == 'POST':
raw_response = self.session.post(url, data=payload, params=parameters)
elif http_method == 'PUT':
raw_response = self.session.put(url, data=payload, params=parameters)
elif http_method == 'DELETE':
raw_response = self.session.delete(url)
result = {
'success': True,
'response': raw_response,
'message': 'Successful {} request to: {}'.format(http_method, url)
}
except RequestException as e:
result = {
'success': False,
'message': 'RequestException {} request to: {} :: {}'.format(http_method, url, e)}
return result
In most cases you will want to have some checks in place to see if a HTTP response code other than the 200-299 range was received. This can indicate congestion or other transient or permanent failure on the server side. You just need a method that you can call to check for that simple condition. In this lab, you won't implement any retries or other handling, but it could be added for specific cases.
def _check_response(self, raw_response):
"""
Evaluates a response status. If it has a non-2XX value, then set the 'success' result
to false and place the exception in the 'message'
:param raw_response: The raw response from a _send_request.
:returns: Returns a the same dictionary passed to it, with the 'success' and 'message'
keys modified, if needed.
:rtype: Dict
"""
pass
def _check_response(self, raw_response):
"""
Evaluates a response status. If it has a non-2XX value, then set the 'success' result
to false and place the exception in the 'message'
:param raw_response: The raw response from a _send_request.
:returns: Returns a the same dictionary passed to it, with the 'success' and 'message'
keys modified, if needed.
:rtype: Dict
"""
try:
# Raise HTTPError error for non-2XX responses
raw_response['response'].raise_for_status()
except HTTPError as e:
raw_response['success'] = False
raw_response['message'] = 'HTTPError Exception: {}'.format(e)
return raw_response
Now you have a method to send the requests and one to check the results for obvious return failures. Next you can implement each individual API to utilize these methods and parse the results according to their own requirements.
For the native REST APIs, this lab will start with Cisco Unity Connection's Provisioning API (CUPI). You have already configured the generic REST portion, and we have extended that class for you with the CUPI-specific components, namely the header and then the response parsing. Specifically, in flaskr/cuc/v1/cupi.py , a CUPI class has been built which extends the REST class using a base_url of /vmrest and a header defined as:
In this CUPI class, we have two methods:
In this section, you will implement the following Unity Connection Provisioning API tasks:
As with Postman, start with something simple: retrieving the Unity Connection version. If you recall, we sent a GET request to /version/product/ , which is what you can recreate here.
Now that you are able to perform CUPI queries, build the front-end API to implement the LDAP users query, to find out who is available to be imported into the system.
When you were working with Postman and, for example, importing users into Unity Connection, you would also retrieve a list of users and then look for the pkid, which you would later use in the POST request to import that specific user. You don't want your front-end web developers to have to worry about something like a pkid or how to retrieve this for each of the product APIs. To perform an import of a user, you really want them to be able to supply only the user ID . In your code on the backend, you can look up the pkid based on that user and perform the import if that user is found.
To import an LDAP user using your API, you must implement the following logic:
You can now implement this logic in the code.
The Unity Connection-related tasks such as modifying and deleting a user should require only the user ID as an input. For that, you need a function that can look up a Unity Connection user account and retrieve their pkid . Luckily, we have already seen that Unity Connection allows for a specific user search using a GET to /vmrest/users with a query parameter such as the following: ?query=(alias is <userid>) . You should put this in its own function, because every one of the subsequent requests will need that exact same lookup.
This will search for and return a specific Unity Connection user, with all their settings, given only the user ID.
Now that you have a way to look up a user account, you can then add the ability to modify them. You may also want to provide a simple flag for a user to reset a mailbox and change a user's PIN. We have handled gathering the input for you. Now, a few API calls in succession will be required to implement user modifications, based on what was supplied. Keep in mind that the credential/PIN changes are a different API method than other user settings.
The following logic will need to be implemented:
Now turn your attention to implementing this in your code.
As with other userid operations, you must first look up the user in order to get its Object ID. Then, if there were no errors, issue the DELETE to remove the account.
You have added a lot of code so far, now it is time to make sure it works as expected.
The response message, is just as you saw with Postman. The difference is that you don't really need to pay attention to the ID assigned, since your code can retrieve this any time using the user ID.
You have a functional CUPI API! Now you are ready to implement the same for Cisco Meeting Server.
As with CUPI, in flaskr/cms/v1/cms.py we have provided a CMS class that extends the REST class, this time with a base_url of /api/v1 and a header as:
A dictionary of error codes is also built, which is pulled from the CMS documentation to provide some more useful error information instead of just a code.
In this CMS class, we also have two methods which provide the same features as the CUPI class, but must process things slightly differently because of the XML return data:
Once the CMS class is built, you can initialize the class globally, as before. CMS actually does not implement any sessions that could be over-utilized. In this case, the Session will still work as before; it simply will never get a JSESSIONID to use as part of the request header. With the class initialized, you can add functions that send requests similar to what you did with Postman, however, you can fine-tune this data to return exactly what you want. For example, the Get Version function will query the /system/status endpoint in CMS as before, but we will only return the version, since that is all that was asked for.
In this lab, we have assumed that if users are assigned personal CMS Spaces, then they will be created with the user ID as the uri , the telephone number as a secondaryURI , and the name of the Space built from the user's display name. These properties could be retrieved from an LDAP directory, but for this lab we chose to implement a UDS-based query using the exact same REST class you already built. It is already initialized as myCUCMuds and only has one callable function, get_user(userid) , that returns a response dictionary with a userName , phoneNumber , and displayName that we can use.
This will allow you to build a POST to create a Space using the information from the data in UDS of the CUCM--and return an error, if that user does not exist in the CUCM.
Just as with CUPI, user modifications/deletions will require you to look up users by user ID. In this lab's case, that means trying to mach a user ID to a URI, a field which you will have populated with the user ID when the Space was created. The challenge is that CMS does not allow you to do a direct lookup. It only allows you to add a filter= parameter, which is always a wildcard filter. In other words, to search for a specific user, you must first search with a filter, then for the results returned, you need to look through each of them to see if the user id you are searching for matches the URI field.
To complicate things further, CMS can throttle responses, limiting the number of objects returned to 10 (per documentation). To retrieve more results than that, you have to implement paging; that is to say repeating the same query, each time adjusting the offset in order to get the next "page."
In this section, you will implement and verify the following Cisco Meeting Server API tasks:
When working with Postman, you retrieved the system status from CMS, which included the version. In this case, you can first write a function to return the CMS version directly, using the information from the system status query.
Using Postman, you were able to retrieve a list of Spaces and an individual Space using the Space ID. But in your web portal, you will want to retrieve, create, update, and delete a CMS space using only a user id, where the user ID is mapped to a URI associated with the Space.
Since all of these tasks require retrieving a list of Spaces and then searching through them to see if the user ID matches a Space URI, you should put that capability in its own function.
Replace the pass line with the following highlighted text in the match_space_uri() function:
Now that you have the ability to look up a Space by the user ID, you just need to add the code to call that function.
Creating a CMS Space was not very difficult in Postman. You simply need to send a POST request to /coSpaces . Even without any other parameters, a Space is created. But for this portal you would like to specify a user ID when creating the Space. With the user ID, you should be able to look up properties about the user. The source of this information might be Active Directory, but here we will use the CUCM's User Data Server (UDS).
THe UDS class we have created for you is another very simple REST service, which can send user searches for user IDs in the CUCM. For this, we have built a get_user() function which takes a user string. Utilizing the same REST class, it returns the same response dictionary containing the following keys:
Modifying a CMS object is much simpler. You use get_coSpace_id() . If the response is successful, then you can send the PUT request to CMS with the retrieved Space ID. Otherwise you can respond an errored user lookup (which could have failed because the user was not found or because there was some error communicating with CMS).
Similar to Space modification, a CMS Space Delete uses get_coSpace_id() to look up the Space's ID which is used to generate a DELETE request to CMS.
You have added a lot of code so far, now it is time to make sure it works as expected.
The "message" contains the object ID of the new Space.
Cisco Meeting Server capabilities could be easily expanded. But now you are ready to take a look at Cisco Webex and notifications.
While AXL API exposes many CUCM Database provisioning tasks, the UC Serviceability XML API is a collection of SOAP-based APIs that expose multiple capabilities for CUCM administrators to automate management and monitoring tasks.
The family of UC Serviceability XML APIs consist of the following 5 APIs:
Each of these APIs has its own dedicated WSDL file that describes its unique methods (service endpoints) and unlike AXL (AXLAPI.wsdl) they are available to download directly from a given CUCM node. Note that all methods (service endpoints) are NOT available in all CUCM releases. See https://developer.cisco.com/site/sxml/documents/operations-by-release/ for more information.
Let's take a closer look at the RisPort70 and Perfmon APIs.
In this section, you will use SoapUI to perform the following tasks:
You should now have two more bindings added to SoapUI project named
UCMSOAP
as shown below. You may have to
collapse the AXLAPIBinding tree to see the other two you just added.
When sending SOAP requests via SoapUI, if you ever get a response payload that indicates " <!--custom Cisco error page--> " and "HTTP Status 401" in the content, double check your Binding Service Endpoint Username & Password
The RisPort70 (Real-Time Information Port) service provides an API for querying the current connection status of phones, devices, and CTI applications connected to the CUCM.
The RisPort70 API documentation is located here: https://developer.cisco.com/docs/sxml/#!risport70-api
This method allows clients to perform Cisco Unified CM device-related queries. The method returns a snapshot of real-time device registration status from each Cisco Unified CM node. The data includes registration status, IP address, model info, and CTI application connections to the device. A single device may appear in multiple elements if the device has registered to multiple nodes. Devices may be requested by wildcard using an asterisk, *.
The Perfmon API is an interface to the CUCM performance counters normally accessed through by the Real-Time Monitoring Tool (RTMT). The PerfMon API allows clients to perform the following tasks:
The session-based counters are useful for tracking information over time. For that, a perfmonOpenSession is required initially to get a session ID. Sessions can be explicitly closed when data collection is complete (best practice) or they will automatically clear if idle for 25 hours. The session ID is subsequently used in queries such as perfmonAddCounter and perfmonCollectSessionData .
See https://developer.cisco.com/docs/sxml/#!perfmon-api for more information on PerfMon API.
In this section, you will perform a single transaction request utilizing the perfmonCollectCounterData operation. You will be querying the Cisco CallManager Performance Counter Class from the CUCM host. This PerfMon API operation request requires not only the Perfmon Counter Class Name (aka Object), but also the specific CUCM Node/Host. For example, this means that even if the SOAP Request is sent to the publisher server in the cluster, the performance class data from any of the other CUCM Cluster nodes can be retrieved. Follow these steps to retrieve performance data using the PerfMon API:
Relevant Webex resources:
Unity Connection has implemented the following APIs:
The difference in CUPI for administrators and CUPI for end users is in the rights required by the account accessing the system. System administration tasks, consequently, require an account with the System Administrator role (configured via the GUI under the User > Roles). Whereas CUPI for end users are accessible to end users for administering their own account, such as greetings, transfer rules, etc.
Aside from the Postman collection we have for testing, the Cisco Unity Provisioning API (CUPI) for administrator and end users has a published Web Application Description Language (WADL) that can be downloaded. This is an XML file that models the resources provided by the web service, similar to what the WSDL file does for the SOAP API. This file can be downloaded from the Unity server using the URL https://{UnityConnectionServer}/vmrest/application.wadl . This isn't available for all APIs, not even all of the Unity Connection APIs, but for these common administrative tasks, having this reference handy is extremely useful. Additionally, a schema is published on the server itself, accessible via https://{UnityConnectionServer}/vmrest/schema . This is especially useful in analyzing the types of data that may be returned for a given query.
Note:
Make sure the entire file is downloaded. Usually it's best to use something like
curl
or
wget
.
For example:
curl -k -u admin:C1sco.123 https://cuc1a.pod22.col.lab/vmrest/application.wadl -o application.wadl
Once downloaded either SoapUI or Postman can import this file (File > Import). In Postman, this loads a Collection, for which all
options are exposed. You can fill in the credentials at the Collection root and then easily perform queries,
checking/unchecking the options you intend to send.
In general, Unity Connection's APIs are fairly flexible. A field that is not understood is always ignored by the server, meaning if some setting is changed that exists in a newer version and that same setting is specified in an older one, the older server will simply ignore that data. As far as the query itself, the server will generally respond back with at 415 if the Content-Type header is not understood and a 302 if there is a connection attempt to an insecure (http) for that is redirected by the server to a secure (https) port.
In this lab, we only have one Unity Connection cluster, but if you have multiple, networked Unity Connection clusters, you can determine which cluster that user is located by querying the globalusers table. For example: GET http://{UnityConnectionServer}>/vmrest/globalusers?query=(alias%20is%jdoe)
For larger data queries, the rowsPerPage and pageNumber can be used to implement paging, thereby reducing the amount of data the server will have to respond with for a query. The query parameter, with its filtering options ( dtmfaccessid startswith 855512 ) can further reduce the amount of data.
Regarding Unity Connection's performance, the system is able to process a maximum of 5 web application requests at one time. The throttle uses a global variable for this, so any request in any process using the throttle will count towards 1 of the 5. Any request that comes in while 5 or more others are processing will get queued and processed as soon as one of the active requests is complete. We can queue up to 255 requests (or the thread limit in tomcat, which is 150). As long as requests process at a rate of 1 request per second, the other requests will remain in the queue. If the requests start processing at a rate slower than 1 per second, the queue wait mechanism times out and the client will get a "server busy" (HTTP 503) response.
For troubleshooting, Unity Connection does provide VMREST micro traces, set on the
Cisco Unity Connection
Serviceability
page under
Traces
>
Micro Traces
. This generally requires
Debug
-level traces and are written to the CUC Tomcat log trace file location (from the CLI:
file list activelog cuc/diag_Tomcat_* date detail
For example a GET request for the /version/product may look like:
10:29:20.548 |20085,,,VMREST,3,DEBUG [http-bio-443-exec-2] com.cisco.connection.rest.SecurityFilter#doFilter - IMS result code: 0 10:29:20.558 |20085,,,VMREST,3,DEBUG [http-bio-443-exec-2] com.cisco.connection.rest.CorsFilter#doFilter - doFilter - The request is not a CORS request as request is from same origin: null . Delegating to next filter for further processing. 10:29:20.558 |20085,,,VMREST,3,DEBUG [http-bio-443-exec-2] com.cisco.connection.rest.RequestFilter#filter - REQUEST GET version/product/ 10:29:22.004 |20085,,,VMREST,3,DEBUG [http-bio-443-exec-2] com.cisco.connection.rest.VersionInformationRestImpl#checkProductVersion - Installed product version: 12.5.1.14900-45
Cisco Meeting Server API access is available via the webadmin feature. This is enabled on a Callbridge server and then uses a user account--either an administrator or minimally an api user. For example, you can add a user for api access via the CLI using the command: user add apiuser api , which then prompts for a password.
Aside from the official API documentation , Cisco Meeting Server has examples and code snippets on Devnet . From there you can download a Postman Collection for most of the CMS methods, as well as a Python SDK called Canopy. These items aren't necessarily maintained by Cisco on a per-release basis, but in general they are very helpful and can be extended for any additional API methods that are introduced. Cisco does maintains interactive API documentation at https://ciscocms.docs.apiary.io . This documentation and tool allows you to run mock queries and get results directly within the framework. At some point in the future, this will likely become the official API documentation repository for CMS.
The documentation does a good job explaining the object hierarchy in CMS, but there are some general considerations to point out with this API:
The following provides some background information on the types of tools we use in this lab as well as tips for working with them.
As mentioned in the lab, we utilized Microsoft Visual Studio Code for code editing. Although the ability to run VS Code remotely, yet be accessed via a browser using code-server may not be relevant for your situation, to assist in debugging and formatting of Python code, you will almost certainly want a Python extension, or the Python Extension Pack , which includes a few Python-related extensions. Without this extension, VS Code can't even help us in syntax-related errors in Python code.
Since VS Code is multi-platform, regardless of the machine you use, once installed, the steps described in the lab here should be very similar, regardless of your specific platform. It is also a good starting off point for any subsequent tools, because in most cases, you'll want to use things that integrate with VS Code so that a single environment can be utilized.
Git is a set of tools to implement a distributed version-control system for tracking source code changes. Used in conjunction with hosting services like GitHub, GitLab, or BitBucket, software projects can be developed and shared with other developers. Even with just one developer, utilizing Git is useful to track changes, revert, and maintain proper versioning.
For this lab, while the lab guide is hosted on one of our servers, the API portal itself is published to a Git repository that is publicly available. That means with the right environment set up, you can download this software, run it and modify it as you wish. Obviously you won't be able to test with the equipment we make available to you, but with very minimal modifications, if you have access to a lab or test equipment, you can modify this to fit your needs.
If your platform does not come with Git, there are several sources not the least of which is the main Git site .
The external repository for our software is at: https://github.com/collabapilab/ltrcol-2574-portal.git
Note there are two branches in this Repo:
Although not absolutely mandatory, understanding and using virtual environment for your project can save you a lot of time in the long run. A virtual environment, as the name implies, is a context in which the code runs. For example, in this project, all of the Python packages required--including their versions--are documented in the requirements.txt file in the root of the repository. It includes things like Flask, zeep, and many other components. If you just install these ( pip install -r requirements.txt ), then you have just installed these and every other Python project that you have on that system hopefully won't have a problem with those exact packages. With virtual environments, you create a separate "environment" just for this project so that you have an isolated environment for all of this software that is not accessible and will not interact with any other virtual environment. This solves dependency problems and allows you to keep track of exactly what your software package requires to run, if you ever need to share it with someone else or install it to run on another server.
There are a number of software packages that can implement virtual environments. A good reference and primer can be found at https://realpython.com/python-virtual-environments-a-primer/
Webex allows for users to set up small, temporary sandbox environments . These can be leveraged for testing of features and code without impacting production Webex organizations.
Postman has many handy features, but for the purposes of code development, it has a feature to generate sample code snippets that can help get you started automating things quickly.
For any request that you generate, you can click on Code at the right, then select the desired language, in our case Python - Requests . The result is an entire software snippet, that can be run in Python.
This section is meant to help you set up an environment like the one in this lab. Obviously it helps to have access to lab equipment, such as a CUCM, Unity Connection, or other servers and devices that you can use to test against.
Since you probably are familiar with setting that part of the infrastructure up, we will focus on the development side of things. These instructions will diverge slightly, depending on the machine and operating system you intend to use for development (e.g. Mac, Windows, Linux). But in general, here are some tips to get up and running:
To get started, you need to make sure your machine is set up properly for Python development.
Once your development machine is up and running, time to clone our repo.
Now you're at the point where a few customizations may be required and then you can run the app.
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Python: hello_world.py", "type": "python", "request": "launch", "program": "${workspaceFolder}/examples/hello_world.py", "console": "integratedTerminal", "justMyCode": true }, { "name": "Python: soap_axl.py", "type": "python", "request": "launch", "program": "${workspaceFolder}/examples/soap_axl.py", "console": "integratedTerminal", "justMyCode": true }, { "name": "Python: soap_sxml.py", "type": "python", "request": "launch", "program": "${workspaceFolder}/examples/soap_sxml.py", "console": "integratedTerminal", "justMyCode": true }, { "name": "Python: wxc_enable_user.py", "type": "python", "request": "launch", "program": "${workspaceFolder}/examples/wxc_enable_user.py", "console": "integratedTerminal", "justMyCode": true }, { "name": "Python: service_app.py", "type": "python", "request": "launch", "program": "${workspaceFolder}/examples/service_app.py", "console": "integratedTerminal", "justMyCode": true }, { "name": "Python: wxm_create_meeting.py", "type": "python", "request": "launch", "program": "${workspaceFolder}/examples/wxm_create_meeting.py", "console": "integratedTerminal", "justMyCode": true }, { "name": "Python: wbx_messages.py", "type": "python", "request": "launch", "program": "${workspaceFolder}/examples/wbx_messages.py", "console": "integratedTerminal", "justMyCode": true }, { "name": "Python: oauth.py", "type": "python", "request": "launch", "program": "${workspaceFolder}/examples/oauth.py", "console": "integratedTerminal", "justMyCode": true }, { "name": "Start LTRCOL-2754 Portal", "type": "python", "request": "launch", "module": "flask", // https://flask.palletsprojects.com/en/1.1.x/config/#configuring-from-environment-variables "env": { "FLASK_APP": "app.py", "FLASK_ENV": "development", "FLASK_DEBUG": "0" }, "args": [ "run", "--host=0.0.0.0", "--no-debugger", "--no-reload" ], "jinja": true } ] }
To remotely connect to the lab using the VPN credentials that have been provided to you by your lab proctors using the Cisco AnyConnect Client, follow these instructions.
If you already have the Cisco AnyConnect client installed, go to the next step. If you do not have the Cisco AnyConnect client installed, navigate to the AnyConnect download page on Cisco.com and download the client for your computer. On Windows, select AnyConnect Pre-Deployment Package (Windows) - includes individual MSI files and on Mac, select AnyConnect Pre-Deployment Package (Mac OS) . Unzip the download (if necessary) and install the client. During the installation process, you only need to install the "Core & VPN" module. None of the other modules are required.
If you are not authorized to download the AnyConnect client using your Cisco.com account, follow these steps for an alternate download.
Once you have the Cisco AnyConnect client installed, follow these steps to connect to the lab.
You will be working on the lab inside of a PC in the lab with all the necessary software pre-installed. You will connect to this PC using a Microsoft Remote Desktop (RDC) connection. Windows comes pre-installed with the client. If you do not have the client for Mac, download it for free from the Mac App Store.
Once you have the RDP client installed, download this file for your Pod 22 lab PC: pod22-studentvm.rdp
Make sure that your VPN connection to the lab is up before double-clicking on the downloaded rdp file. You will be prompted for a password. Use c1sco123 for the password.
The lab manual is available in the web browser on your RDP session, however if you would like to view the lab manual on your local PC, you navigate to the lab manual for your pod 22 by clicking below. Please be sure to connect to the correctly assigned pod number: https://collabapilab.ciscolive.com/lab/pod22/intro/landing .
To remotely connect to the lab using the VPN credentials that have been provided to you by your lab proctors using the native Windows VPN client, follow these instructions.
Step 1: Open Windows Settings by searching for settings in the Windows search box and select the Settings app.
Step 2: Open VPN Settings by searching for VPN in the Windows Settings box and select VPN settings from the menu.
Step 3: Click the Add a VPN connection button.
Step 4: In the Add a VPN connection window, configure the following:
Step 5: You should now see a VPN profile for HOLCOL-2574, but you will not be able to connect yet. Click on Change adapter options .
Step 6: The Network Connections window should appear. Right-click on HOLCOL-2564 and select Properties .
Step 7: In the properties window, select the Security tab, select the option for Allow these protocols and make sure only Microsoft CHAP Version 2 (MS-CHAP v2) is selected.
Step 8: To start the VPN connection, double-click on the HOLCOL-2574 icon in the Network Connections window.
Step 9: A menu will pop up and at the top you should see a VPN option for HOLCOL-2574 . Click the Connect button to initiate the VPN connection. If you did not provide your username and password when you configured the connection, you will be prompted to enter your username and password.
Step 10: Once connected, you can come to the same place to disconnect your connection.
You will be working on the lab inside of a PC in the lab with all the necessary software pre-installed. You will connect to this PC using a Microsoft Remote Desktop (RDC) connection. Windows comes pre-installed with the client. If you do not have the client for Mac, download it for free from the Mac App Store.
Once you have the RDP client installed, download this file for your Pod 22 lab PC: pod22-studentvm.rdp
Make sure that your VPN connection to the lab is up before double-clicking on the downloaded rdp file. You will be prompted for a password. Use c1sco123 for the password.
The lab manual is available in the web browser on your RDP session, however if you would like to view the lab manual on your local PC, you navigate to the lab manual for your pod 22 by clicking below. Please be sure to connect to the correctly assigned pod number: https://collabapilab.ciscolive.com/lab/pod22/intro/landing .
To remotely connect to the lab using the VPN credentials that have been provided to you by your lab proctors using the native Windows VPN client, follow these instructions.
Step 1: Open System Preferences and select the Network System Preference. Click on the + on the bottom left corner of the window.
Step 2: In the window that appears, configure the following:
Step 3: On the settings page, configure the following:
Step 4: On the window that appears, configure the following:
Step 5: Before connecting, click Apply . Now click the Connect button and your connection should be established.
You will be working on the lab inside of a PC in the lab with all the necessary software pre-installed. You will connect to this PC using a Microsoft Remote Desktop (RDC) connection. Windows comes pre-installed with the client. If you do not have the client for Mac, download it for free from the Mac App Store.
Once you have the RDP client installed, download this file for your Pod 22 lab PC: pod22-studentvm.rdp
Make sure that your VPN connection to the lab is up before double-clicking on the downloaded rdp file. You will be prompted for a password. Use c1sco123 for the password.
The lab manual is available in the web browser on your RDP session, however if you would like to view the lab manual on your local PC, you navigate to the lab manual for your pod 22 by clicking below. Please be sure to connect to the correctly assigned pod number: https://collabapilab.ciscolive.com/lab/pod22/intro/landing .