You're a Python aficionado. Want to build a Python AXL administration and configuration app with a GUI? Have I got the solution for you. No, it's not Tkinter or wxWidgets, which is based on the Gimp Toolkit (GTK). We're talking Qt, one of the most popular frameworks for multiple platforms. wxWidgets has similar features, but I've never been a fan of GTK.
Good question. Way back when I was editor-in-chief of Linux Journal, there was a big push for an Outlook clone called Evolution. Evolution is a C#app on Mono, a Linux clone of .NET. I considered this a misguided effort and said as much. It's a fool's errand to perpetually chase feature parity with Microsoft's offerings. And back then when Internet dinosaurs roamed the earth, the world was already moving toward Software as a Service (SaaS) for user-facing applications. If you like Evolution, more power to you, but even Microsoft has come around to SaaS. The Office 365 web-based suite is excellent. I can use my work Outlook account on the web. And I use my personal Office 365 Outlook account from within a web browser on Windows and Linux. Instant platform-neutral access to Outlook and no need for Mono's nucleosis.
As much as I pushed SaaS back then, here's where I part ways. AXL is an administration and configuration API for Cisco UCM, not an API for user-facing apps. The way I see it, if you want to do administration of CUCM on the web, there's the web GUI that comes with CUCM. When you want a custom administration app, I prefer an AXL application with a GUI native to the OS. Qt is a GUI framework that works on Windows, Mac, and Linux. Best of all, Monty's pinnacle of achievement, Python, runs on all these platforms, and there's a Python version of Qt called PyQt. Throw in a little Zeep, and you've got a handy Python SOAP AXL interface with a modern Qt UI.
All of the following examples are on Windows. Some of the commands will be different on a Mac or Linux, but it all works. If you want to take a gander at the finished sample app, you can get it from GitHub. I chose version 6, PyQt6 for this app, but you can use earlier versions and adjust the code accordingly. I'm using a generic ">" command line prompt. Your prompt will be different. If coding in Python with Zeep is new to you, or you want more details on differences between platforms, check out this learning lab.
First, I create a virtual Python environment.
>python -m venv PyQtAXL
I create a Project directory, change to the directory and activate the environment. Then I install the libraries we need.
>cd PyQtAXL
>mkdir Project>cd Project
>..\Scripts\activate
>pip install zeep
>pip install PyQt6
I get a message that pip isn't the latest version, so I update it.
>python -m pip install --upgrade pip
Now I'm ready to start coding a simple getUser app, which I'll call GetUser.py.
Here's what the app looks like when you first launch it (I entered my userid):
The PyQt6 App ready to search for a userI click "Search Username" and see this:
The Search ResultsClick "Reset" and poof, the info disappears. (You could resize the window, too, but I didn't bother.)
Back to the Search screenThe nice thing about PyQt6 is that all I closed with the Reset button is the "Results" group box. That object goes away, along with all the child objects inside the box, including even the reset button. So, you can click "Search Username" and "Reset" as many times as you like and not experience any ill effects.
Now let's look at the code. Don't copy and paste anything that follows. There's a lot more to this app than I'm showing here. We'll see the entire app later. This section is an abbreviated explanation of how it works.
The AXL request looks like this:
criteria = { 'userid' : userid } response = self.service.getUser(**criteria)
If you want to see the entire JSON response, add aprint(response)
in your code and the entire response will show in the CMD window where you run the app. For our purposes, I will show only the data we plan to use:
{ 'return': { 'user': { 'firstName': 'nicholas', 'displayName': 'nicholas petreley', 'middleName': None, 'lastName': 'petreley', 'userid': 'nicholas', 'associatedDevices': { 'device': [ 'CSFNicholas', 'SEP2C31246D2458', 'SEP5475D0785AD4' ] }...
Let's do some Python to get the user values we want to show (we'll get fancy with this later when we use PyQt6 -this is just straight Python, extracting data from the dictionary and list in the above JSON):
d = response['return']['user'] fname = d['firstName'] lname = d['lastName'] dname = d['displayName'] ad = response['return']['user']['associatedDevices']['device'] for value in ad: values[ad.index(value)] = value
Now we need some way to display all this data in a GUI app using the Qt framework. We'll create a window with an input field where you will type a username, and a button that tells the app to search for that user. Like any good platform-neutral framework, Qt uses layouts. Layouts make it possible for an app to look good without having to specify the location of objects in X,Y coordinates or specify sizes. In this case, we'll use a horizontal layout, with the search button to the left of the input field.
class MainWindow(QWidget): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.initUI() def initUI(self): button = QPushButton("Search Username") self.input = QLineEdit() hbox = QHBoxLayout() hbox.addWidget(button) hbox.addWidget(self.input) self.input.setText('nicholas') hbox.addStretch(1) button.clicked.connect(self.search_user)
As you can see, I preload the QLineEdit field,self.input.setText('nicholas')
, with my name so I don't have to type it when I run the app. The sample app doesn't have that line of code.
Note that I connect the button clicked event to a method calledself.search_user
. More on that later. Right now, we need a place to put the search results and a way to wrap this up with a window title. Let's create a vertical box layout for the results, add the title, and then do the obligatory "show" command so you can see what we've done.
self.vbox = QVBoxLayout() self.vbox.addLayout(hbox) self.vbox.addStretch(1) self.setLayout(self.vbox) self.setWindowTitle("getUser") self.show()
We're going to show the search results in the method called "self.search_user". To make the results fancy pants, we'll put them in one "results" group box that contains two more group boxes; one for the name data, the other for the associated devices.
self.resBox = QGroupBox("Results")... self.groupBox = QGroupBox("User")... self.adGroup = QGroupBox("Associated Devices")
Each box has its own layout. The results box will be a simple layout that puts its children in a vertical stack.
self.resBox = QGroupBox("Results") self.vresBox = QVBoxLayout() self.resBox.setLayout(self.vresBox)
We want labels to the left of values in the name data, so we'll use a grid layout with two columns for that. We specify the columns when we add the data, not when we define the grid.
self.groupBox = QGroupBox("User") self.gridBox = QGridLayout() self.groupBox.setLayout(self.gridBox)
When we put the data in that grid, we specify the row and column for labels and data. The first label,self.fl
, goes in row 0, column 0. The first data fieldself.fe
goes in row 0, column 1. And so on.
self.fl = QLabel('First Name') self.fe = QLineEdit(d['firstName']) self.ll = QLabel('Last Name') self.le = QLineEdit(d['lastName']) self.dl = QLabel('Display Name') self.de = QLineEdit(d['displayName']) self.groupBox.setLayout(self.gridBox) self.gridBox.addWidget(self.fl,0,0) self.gridBox.addWidget(self.fe,0,1) self.gridBox.addWidget(self.ll,1,0) self.gridBox.addWidget(self.le,1,1) self.gridBox.addWidget(self.dl,2,0) self.gridBox.addWidget(self.de,2,1)
The list of associated devices is a simply vertical list box. We'll make them all QLineEdit fields.
self.adGroup = QGroupBox("Associated Devices") self.advbox = QVBoxLayout() self.adGroup.setLayout(self.advbox) for value in ad: values[ad.index(value)] = QLineEdit(value) self.advbox.addWidget(values[ad.index(value)])
Now let's add the reset button and connect it to theself.resetBox
method.
self.resetButton = QPushButton("Reset") self.resetButton.clicked.connect(self.resetBox)
Now we embed all the widgets where they go. We add the name data boxself.groupBox
to the vertically oriented results box,self.vresBox
, which we defined above. We add the associated devices box self.adGroup to the sameself.vresBox
. We add the reset button to the results boxself.vresBox
. And to wrap things up, we add the results boxself.resBox
to the main UI vertical box we defined much earlier,self.vbox
. Tell the app to show what we have, and voila, we see the results screen.
self.vresBox.addWidget(self.groupBox) self.vresBox.addWidget(self.adGroup) self.vresBox.addWidget(self.resetButton) self.vbox.addWidget(self.resBox) self.show()
Now we just need to add the method that resets the results:
def resetBox(self): self.resBox.close()
As you can see, all we have to close is the results group box that contains all the results data children.
Now it's time to see the whole enchilada, including the Zeep SOAP code. You'll see that things aren't exactly in the above order. The above examples are meant to show how PyQt6 works. In actual practice, the code can be shuffled around as desired. Also, using "import *" is frowned upon in Python, but I used it with PyQt6 in this code to make it simpler to read. When you write your own app, you should specify the widgets and objects you intend to use from the libraries.
from PyQt6.QtGui import *from PyQt6.QtWidgets import *from PyQt6.QtCore import *from lxml import etreefrom requests import Sessionfrom requests.auth import HTTPBasicAuthfrom zeep import Client, Settings, Pluginfrom zeep.transports import Transportfrom zeep.cache import SqliteCacheimport argparseimport sys#Parse command line arguments.#Overwrite them with values for your own CUCM using or use command line argsdef parse_args(): parser = argparse.ArgumentParser() parser.add_argument('-u', dest='username', help="AXL username", required=False, default='administrator_username') parser.add_argument('-p', dest='password', help="AXL user password", required=False, default='administrator_password') parser.add_argument('-s', dest='server', help="CUCM hostname or IP address", required=False,default='your_ucm_server') args = parser.parse_args() return vars(args)class MainWindow(QWidget): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.initUI()#The WSDL is a local file WSDL_URL = 'AXLAPI.wsdl'#Parse command line arguments cmdargs = parse_args() USERNAME, PASSWORD, SERVER = ( cmdargs['username'], cmdargs['password'], cmdargs['server'] ) CUCM_URL = 'https://' + SERVER + ':8443/axl/'#This is where the meat of the application starts#The first step is to create a SOAP client session session = Session()#We avoid certificate verification by default, but you can set your certificate path#here instead of the False setting session.verify = False session.auth = HTTPBasicAuth(USERNAME, PASSWORD) transport = Transport(session=session, timeout=10, cache=SqliteCache())#strict=False is not always necessary, but it allows zeep to parse imperfect XML settings = Settings(strict=False, xml_huge_tree=True) client = Client(WSDL_URL, settings=settings, transport=transport) self.service = client.create_service("{http://www.cisco.com/AXLAPIService/}AXLAPIBinding", CUCM_URL) def initUI(self): button = QPushButton("Search Username") self.input = QLineEdit() hbox = QHBoxLayout() hbox.addWidget(button) hbox.addWidget(self.input) self.input.setText('nicholas') button.clicked.connect(self.search_user) self.vbox = QVBoxLayout() self.vbox.addLayout(hbox) self.vbox.addStretch(1) self.setLayout(self.vbox) self.setWindowTitle("getUser") self.show() def search_user(self): userid = self.input.text() criteria = { 'userid' : userid } response = self.service.getUser(**criteria) self.resBox = QGroupBox("Results") self.vresBox = QVBoxLayout() self.resBox.setLayout(self.vresBox) d = response['return']['user'] self.fl = QLabel('First Name') self.fe = QLineEdit(d['firstName']) self.ll = QLabel('Last Name') self.le = QLineEdit(d['lastName']) self.dl = QLabel('Display Name') self.de = QLineEdit(d['displayName']) self.groupBox = QGroupBox("User") self.gridBox = QGridLayout() self.groupBox.setLayout(self.gridBox) self.gridBox.addWidget(self.fl,0,0) self.gridBox.addWidget(self.fe,0,1) self.gridBox.addWidget(self.ll,1,0) self.gridBox.addWidget(self.le,1,1) self.gridBox.addWidget(self.dl,2,0) self.gridBox.addWidget(self.de,2,1) self.adGroup = QGroupBox("Associated Devices") self.advbox = QVBoxLayout() self.adGroup.setLayout(self.advbox) ad = response['return']['user']['associatedDevices']['device'] values = dict() for value in ad: values[ad.index(value)] = QLineEdit(value) self.advbox.addWidget(values[ad.index(value)]) self.resetButton = QPushButton("Reset") self.resetButton.clicked.connect(self.resetBox) self.vresBox.addWidget(self.groupBox) self.vresBox.addWidget(self.adGroup) self.vresBox.addWidget(self.resetButton) self.vbox.addWidget(self.resBox) self.show() def resetBox(self): self.resBox.close()def main(): app = QApplication(sys.argv) su = MainWindow() sys.exit(app.exec())if __name__ == '__main__': main()app.exec()
You can grab the above sample code here. Make sure to download the AXL SQL toolkit from UCM and put the appropriate WSDL and XSD files in the same directory as the app. Instructions on how to do that are included in the Python/Zeep Learning Lab.
Once you get the hang of using PyQt6, it's a very pleasing API to work with. You'll quickly develop complex prototype applications that you can promote to production apps almost as fast.
Resources:
PyQt6 Documentation
Python/Zeep Learning Lab
GetUser.py sample app on GitHub