Extreme API
Extreme API
1 Preface .......................................................................................... 5
1.1 References .................................................................................................................................... 5
1.2 Acknowledgements ....................................................................................................................... 5
2 Introduction .................................................................................. 6
2.1 Using Python ................................................................................................................................. 6
2.1.1 Install Python ........................................................................................................................ 6
2.1.2 Update Your PATH ................................................................................................................ 6
2.1.3 Virtual Environment .............................................................................................................. 7
2.1.4 PIP ......................................................................................................................................... 7
2.1.5 Editors and IDE ...................................................................................................................... 7
2.2 REST APIs ....................................................................................................................................... 8
2.2.1 URLs....................................................................................................................................... 8
2.2.2 HTTP Status Codes ................................................................................................................ 9
2.2.3 HTTP Request Methods......................................................................................................... 9
2.2.4 HTTP Headers ........................................................................................................................ 9
2.2.5 Manipulating Headers with Python .................................................................................... 11
2.3 Authentication and Authorization .............................................................................................. 12
2.3.1 Basic Authentication ........................................................................................................... 12
2.3.2 Bearer Authentication......................................................................................................... 12
2.3.3 API Key ................................................................................................................................ 13
2.3.4 OAuth 2.0 ............................................................................................................................ 13
2.3.5 Managing Passwords or Tokens with Python ..................................................................... 13
2.4 Understanding JSON ................................................................................................................... 15
2.5 Manipulating JSON with Python ................................................................................................. 16
2.6 Interact with a REST API using Python ........................................................................................ 18
2.6.1 Urllib .................................................................................................................................... 18
2.6.2 Requests .............................................................................................................................. 20
2.6.3 Testing a REST API ............................................................................................................... 25
2.7 Webhooks ................................................................................................................................... 27
2.8 HTTPS with Python ...................................................................................................................... 28
3 EXOS APIs.................................................................................... 29
3.1 On-Switch APIs ............................................................................................................................ 29
3.1.1 Python Scripting .................................................................................................................. 29
3.1.2 Python Application .............................................................................................................. 35
3.2 External APIs ............................................................................................................................... 40
3.2.1 RESTCONF API ..................................................................................................................... 41
3.2.2 JSON-RPC API ...................................................................................................................... 49
Page |5
1 Preface
This document is provided for information only, to share technical experience and knowledge. Do not
use this document to validate designs, features, or scalability.
1.1 References
The following references are used extensively in the preparation of this document:
EXOS 30.6 User Guide
EXOS 30.6 RestConf Developer Guide
XMC 8.4.3 GraphQL API
Configuring User Interfaces and Operating Systems for VOSS 8.1.5
ExtremeCloud IQ Developer Portal
Engineering API/Application Documentation
Extreme Developer Center
Extreme Networks product documentation (software)
HTTPS://www.python.org/
HTTPS://www.openconfig.net/
1.2 Acknowledgements
Document author: Stéphane Grosjean, Principal Systems Engineer
Content in this document is based on training modules provided by Markus Nispel and lab guides
developed by the Extreme Systems Engineering team.
Page |6
2 Introduction
This document provides an easy approach to the various APIs within Extreme Networks solutions.
The programming language used here is Python 3, but other languages can also be used. The Python 3
was selected based on how easy it is for beginners to learn, and its wide use in the market.
Note: On the Ubuntu output, we have both Python 2 and Python 3 installed. To differentiate
between the two, you need to type either python (for Python 2) or python3 (for Python 3).
Page |7
2.1.4 PIP
The best tool for installing new modules and libraries is PIP, which is highly recommended, and installed
by default with Python since release 3.4.
HTTPS://docs.python.org/3/installing/index.html
Page |8
Note: If you need more information about a function, method or library, Python has a built-in help
system. In the Python interactive shell, type help(<method>) or dir(<method>.
If you use an IDE, you can run code directly from the editor and, with more advanced IDEs, you can
manage virtual environments and execute selected portions of code. Spyder and Jupyter are part of the
Anaconda distribution, which is a typical environment for data science.
2.2.1 URLs
URL stands for Uniform Resource Locator, which is a string you enter in a browser, typically to access a
site. This string contains a great deal of information.
HTTPS://en.wikipedia.org/wiki/JSON?key=value&data=info#Example
For example, this URL can be broken down into the following elements:
The protocol, which can be HTTPS, HTTP, ftp, etc.
The host, often an IP address, which is the location of the server you want to reach.
The host is sometimes followed by “:” and a value, which is the port number. If this is not
present, then the browser defaults to the default protocol value (HTTP = 80, HTTPS = 443, for
example).
The path at the destination server to reach the content. In the context of a REST API, this is
often called an endpoint.
A query string follows an optional ?, and is used to pass arguments to the server, typically in
name=value pairs. To pass several arguments, use the & character to separate them.
A fragment appears after an optional #, and leads directly to a given part of the content.
Page |9
P a g e | 10
Wen working with REST API, (and any API using HTTP), you will most likely need to manipulate the
headers using the content-type, accept, authorization and x-auth-token commands.
Note: The HTTP Archive site is an excellent resource for learning more about HTTP. This site
monitors the top 1.3M web sites and extracts the HTTP information for analyses. This
information, along with reports such as State of the Web , are accessible to the public.
2.2.4.1 Content-Type
The content-type indicates, as the name implies, what is the format of the data in the body. This is a
very important piece of knowledge, as you would not treat that data the same way if this is pure text
html, some binary form or some JSON data for an application.
In your context of a REST API, you’ll most likely use the “application/json” value for this parameter, as
long as you are, indeed, transmitting data in JSON format.
Content-Type: application/json
HTTPS://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
2.2.4.2 Accept
The client uses the Accept header to advertise which content types it can understand. The server
informs the client of the choice using the Content-Type header. In REST API, the accept header is often
set to “application/json”.
Accept: application/json
HTTPS://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
2.2.4.3 Authorization
The authorization request header contains the credentials required to authenticate a user agent (a
client) with a server. The authorization header value format is <type> <credential> with a space
between them.
The typical Basic authentication type concatenates the username and the password in a single string,
separated by a colon (:). This means that a username cannot also contain a colon. The result is encoded
in base64. This is not an encryption because it is reversible.
To access an online tool that can encode and decode in base64, visit: HTTPS://www.base64encode.org/
As an example, the string stef:extreme is encoded in base64 as c3RlZjpleHRyZW1l.
authorization: Basic c3RlZjpleHRyZW1l
HTTPS://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
P a g e | 11
2.2.4.4 X-Auth-Token
The X-Auth-Token is an unregistered header and is not subject to formal specification. Its presence and
content are always tied to a respective application. It typically stores a token for authorization and can
be considered as a shortcut of the bearer token defined in OAuth 2.0.
For Extreme APIs, the X-Auth-Token will be used with EXOS and VOSS Restconf implementation.
r = requests.get("HTTPS://api.nasa.gov/planetary/apod")
headers = {
'content-type': 'application/json',
'x-auth-token': 'c3RlZjpleHRyZW1l'
}
r = requests.get("HTTPS://httpbin.org/get", headers=headers)
P a g e | 12
P a g e | 13
bearer scheme was created as part of OAuth 2.0 (rfc 6750), but is sometimes used alone. As the token
must remain secret, the best practice is to use it only with HTTPS.
P a g e | 14
password = getpass.getpass()
You entered:
Username: Stef Password: extreme
If the application is running on a secure environment, another way to is to store passwords, keys, and
tokens as environment variables that the application can access. How you create environment variables
will depend on your operating system:
- In Windows, create environment variables from Control Panel > System > Advanced System
Settings > Environment Variables.
- With MacOS and Linux, add variables in the .batch_profile file, located in your home directory.
The syntax is export <Var Name>=”<Value>”. There are no spaces between the variable name, the
= sign and the value.
You can then access these environment variables with the OS module.
In this example, you have created (on Windows 10) 2 environment variables:
- MY_USER
- MY_PASSWORD
You can retrieve them from Python, without exposing them in the code:
import os
username = os.environ.get('MY_USER')
password = os.environ.get('MY_PASSWORD')
P a g e | 15
P a g e | 16
information that can be read by humans, JSON is currently the most efficient tool. Nevertheless, when
you need to exchange a vast amount of information, performance and efficiency become more
important and you can consider new standards. These alternatives are not yet used in external Extreme
APIs, although binary formats, such as Protobuf, are becoming popular as well. JSON and Protobuf are
expected to co-exist, serving different needs.
JSON Python
Object dict
Array list
String str
number (int) int
number (real) float
true True
false False
null None
HTTPS://docs.python.org/3/library/json.html#encoders-and-decoders
View the functions and methods available with JSON using the print(dir(json)) command:
import json
print(dir(json))
The result:
['JSONDecodeError', 'JSONDecoder', 'JSONEncoder', '__all__', '__author__',
'__builtins__', '__cached__', '__doc__', '__file__', '__loader__',
'__name__', '__package__', '__path__', '__spec__', '__version__',
'_default_decoder', '_default_encoder', 'codecs', 'decoder',
'detect_encoding', 'dump', 'dumps', 'encoder', 'load', 'loads', 'scanner']
The methods most often used are highlighted in this example. The following examples illustrate how to
use some of these methods.
P a g e | 17
import json
json_sample = '''
{
"whisky": [
{
"name": "Hibiki",
"type": "Blended",
"age": 17
},
{
"name": "Old Pulteney",
"type": "Single Malt",
"age": 21
}
],
"stock": null,
"alcohol": true
}
'''
data = json.loads(json_sample)
print(type(data))
print(data)
new_data = json.dumps(data)
print(type(new_data))
print(new_data)
This example imports the JSON module and manipulates a JSON entry in Python. You must first
transform it to an editable dictionary, then reconvert it to JSON format. The null and Boolean values
change accordingly.
<class 'dict'>
{'whisky': [{'name': 'Hibiki', 'type': 'Blended', 'age': 17}, {'name': 'Old
Pulteney', 'type': 'Single Malt', 'age': 21}], 'stock': None, 'alcohol':
True}
<class 'str'>
P a g e | 18
In this example, a string is the source, but you could also have uploaded information from a file, and
saved it back to a file using the json.load() and json.dump() commands.
2.6.1 Urllib
When you are working with Python, you can access HTTP or HTTPS URLs using the standard (included)
Urllib package. For details about how to use Urllib, see the official documentation, or use any of the
many tutorials available online.
HTTPS://docs.python.org/3/library/urllib.html
Urllib has several modules, the request module being the most useful.
HTTPS://docs.python.org/3/library/urllib.request.html#module-urllib.request
print(dir(request))
The output is shown below, with the most useful function highlighted.
['AbstractBasicAuthHandler', 'AbstractDigestAuthHandler',
'AbstractHTTPHandler', 'BaseHandler', 'CacheFTPHandler',
'ContentTooShortError', 'DataHandler', 'FTPHandler', 'FancyURLopener',
'FileHandler', 'HTTPBasicAuthHandler', 'HTTPCookieProcessor',
'HTTPDefaultErrorHandler', 'HTTPDigestAuthHandler', 'HTTPError',
'HTTPErrorProcessor', 'HTTPHandler', 'HTTPPasswordMgr',
'HTTPPasswordMgrWithDefaultRealm', 'HTTPPasswordMgrWithPriorAuth',
'HTTPRedirectHandler', 'HTTPSHandler', 'MAXFTPCACHE', 'OpenerDirector',
'ProxyBasicAuthHandler', 'ProxyDigestAuthHandler', 'ProxyHandler', 'Request',
'URLError', 'URLopener', 'UnknownHandler', '__all__', '__builtins__',
'__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__',
'__spec__', '__version__', '_cut_port_re', '_ftperrors', '_have_ssl',
'_localhost', '_noheaders', '_opener', '_parse_proxy',
'_proxy_bypass_macosx_sysconf', '_randombytes', '_safe_gethostbyname',
'_thishost', '_url_tempfiles', 'addclosehook', 'addinfourl', 'base64',
'bisect', 'build_opener', 'contextlib', 'email', 'ftpcache', 'ftperrors',
P a g e | 19
The following example shows how to use urlopen, and how to find other functions:
from urllib import request
resp = request.urlopen('HTTPS://www.youtube.com')
print(dir(resp))
Perform a dir() of the object returned from request.urlopen to see more functions.
['__abstractmethods__', '__class__', '__del__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_abc_impl',
'_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable',
'_check_close', '_close_conn', '_get_chunk_left', '_method', '_peek_chunked',
'_read1_chunked', '_read_and_discard_trailer', '_read_next_chunk_size',
'_read_status', '_readall_chunked', '_readinto_chunked', '_safe_read',
'_safe_readinto', 'begin', 'chunk_left', 'chunked', 'close', 'closed',
'code', 'debuglevel', 'detach', 'fileno', 'flush', 'fp', 'getcode',
'getheader', 'getheaders', 'geturl', 'headers', 'info', 'isatty', 'isclosed',
'length', 'msg', 'peek', 'read', 'read1', 'readable', 'readinto',
'readinto1', 'readline', 'readlines', 'reason', 'seek', 'seekable', 'status',
'tell', 'truncate', 'url', 'version', 'will_close', 'writable', 'write',
'writelines']
resp = request.urlopen('HTTPS://www.python.org')
print(resp.code)
print(resp.length)
data = resp.read()
P a g e | 20
print(type(data))
print(len(data))
The result is shown below:
200
48959
<class 'bytes'>
48959
This example shows the success HTTP status code (200), and the amount of data returned in bytes.
2.6.2 Requests
Although Urllib provides all the required tools to manipulate URLs and HTTP CALLs, a better package
called Requests is commonly used.
HTTPS://requests.readthedocs.io/en/master/
The best practice is to install Requests with PIP.
Note: The Requests module is part of XMC Python scripting engine and EXOS Python scripting capability.
The following example creates a virtual environment in Windows 10 to demonstrate how to create and
activate a venv.
C:\> python -m venv "c:\Extreme API with Python"
(Extreme API with Python) C:\Extreme API with Python> pip install requests
Collecting requests
P a g e | 21
Downloading
HTTPS://files.pythonhosted.org/packages/1a/70/1935c770cb3be6e3a8b78ced23d7e0f3b187f5cb
fab4749523ed65d7c9b1/requests-2.23.0-py2.py3-none-any.whl (58kB)
|████████████████████████████████| 61kB 1.9MB/s
Collecting certifi>=2017.4.17 (from requests)
Downloading
HTTPS://files.pythonhosted.org/packages/57/2b/26e37a4b034800c960a00c4e1b3d9ca5d7014e98
3e6e729e33ea2f36426c/certifi-2020.4.5.1-py2.py3-none-any.whl (157kB)
|████████████████████████████████| 163kB 6.4MB/s
Collecting idna<3,>=2.5 (from requests)
Downloading
HTTPS://files.pythonhosted.org/packages/89/e3/afebe61c546d18fb1709a61bee788254b40e736c
ff7271c7de5de2dc4128/idna-2.9-py2.py3-none-any.whl (58kB)
|████████████████████████████████| 61kB 4.1MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 (from requests)
Downloading
HTTPS://files.pythonhosted.org/packages/e1/e5/df302e8017440f111c11cc41a6b432838672f5a7
0aa29227bf58149dc72f/urllib3-1.25.9-py2.py3-none-any.whl (126kB)
|████████████████████████████████| 133kB 3.3MB/s
Collecting chardet<4,>=3.0.2 (from requests)
Using cached
HTTPS://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec751
0b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl
Installing collected packages: certifi, idna, urllib3, chardet, requests
Successfully installed certifi-2020.4.5.1 chardet-3.0.4 idna-2.9 requests-2.23.0
urllib3-1.25.9
To better illustrate its use, you can create some GET and POST examples. A good resource for making
HTTP CALLs is HTTPS://httpbin.org which is a simple HTTP request and response service. These can be
considered the first REST API CALLs.
Note: The httpbin.org service has been written by the same author than the Requests module.
Examine the GET method. The response content type is set to application/json by default. Keep this
setting so that you can manipulate JSON data.
P a g e | 22
Make a REST CALL, using GET to retrieve data. This service lets you add parameters (arguments) to the
URL in a query string, so that the server also returns this information .
Requests has an integrated JSON function that you can use also, as shown below :
import requests
print(r.url)
print(r.status_code)
print(r.headers['content-type'])
print(r.encoding)
print(r.text)
data = r.json()
print(type(data))
print(data)
In this example, the query string has been separated from the URL. You could have added the
parameters directly to the URL, but it is a good practice to work this way, which allows you to reuse the
same URL with different parameters. When you test using this method, you use the GET function with
requests and the GET path on httpbin.org.
The result of a test is the shown below:
P a g e | 23
https://httpbin.org/get?h2g2=42&elite=1337
200
application/json
None
{
"args": {
"elite": "1337",
"h2g2": "42"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0",
"X-Amzn-Trace-Id": "Root=1-5ed8c610-484d6854daf7112485a3b020"
},
"origin": "109.13.132.180",
"url": "https://httpbin.org/get?h2g2=42&elite=1337"
}
<class 'dict'>
{'args': {'elite': '1337', 'h2g2': '42'}, 'headers': {'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent':
'python-requests/2.22.0', 'X-Amzn-Trace-Id': 'Root=1-5ed8c610-
484d6854daf7112485a3b020'}, 'origin': '109.13.132.180', 'url':
'https://httpbin.org/get?h2g2=42&elite=1337'}
Next, send data to the service by using the POST method from requests and changing the path to the
service to POST. Because you are now sending data to the service, you must remove the params
keyword and replace it with the data keyword, as shown below:
import requests
print(r.url)
print(r.status_code)
print(r.headers['content-type'])
print(r.text)
data = r.json()
P a g e | 24
print(type(data))
print(data)
In the results, you can see that the URL no longer contains a query string, and in the JSON returned,
there is a form entry with the data you sent.
HTTPS://httpbin.org/post
200
application/json
{
"args": {},
"data": "",
"files": {},
"form": {
"elite": "1337",
"h2g2": "42"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "17",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0",
"X-Amzn-Trace-Id": "Root=1-5ed8cb37-4c412bacebb72bbcaf3e5bfc"
},
"json": null,
"origin": "109.13.132.180",
"url": "HTTPS://httpbin.org/post"
}
<class 'dict'>
{'args': {}, 'data': '', 'files': {}, 'form': {'elite': '1337', 'h2g2':
'42'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate',
'Content-Length': '17', 'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.22.0', 'X-Amzn-Trace-
Id': 'Root=1-5ed8cb37-4c412bacebb72bbcaf3e5bfc'}, 'json': None, 'origin':
'109.13.132.180', 'url': 'HTTPS://httpbin.org/post'}
Another option when using the requests.get function is the timeout parameter. Without this parameter,
the requests module waits indefinitely for an answer, which can be a problem if you have made a
mistake. This can also result in very slow server speeds and you don’t want the application to spend too
much time waiting. You can set a limit before raising an error. Httpbin can help you to simulate this,
with the delay service in the dynamic data menu. To call it, add /delay/<value in seconds> to the URL.
For example:
import requests
P a g e | 25
r = requests.get('HTTPS://httpbin.org/delay/4', timeout=3)
Add a delay of 4 seconds for the answer with a timeout of 3 seconds. Running the script gives you a
traceback:
Traceback (most recent call last):
[…]
raise ReadTimeout(e, request=request)
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='httpbin.org',
port=443): Read timed out. (read timeout=3)
You can also use a Python try/except, which provides a cleaner result without breaking code.
import requests
try:
r = requests.get('HTTPS://httpbin.org/delay/4', timeout=3)
except requests.exceptions.Timeout:
print("Server is too long to answer")
P a g e | 26
These sections are the Request builder, the Response window, and the Explorer window. In the Request
builder, you create the HTTP CALL, specify the URL, add parameters, and set the authentication and
headers. The Request and Response windows display requests and responses.
Zoom into the Request builder, to see (in this example) an HTTP GET method, the URL for the API next to
it, and several tabs where you can personalize the CALL. In this example, no authorization is necessary
(this is very rare) so you just must set JSON as the application.
Select Send to see the response from the API.
P a g e | 27
In the response window, you can see the HTTP Status code, in this case an encouraging 200, and the
data sent back in JSON by the API. You can now use this information in the application, as you can see
the keys and value types returned.
You can now write a Python application to interface with this API.
import requests
if r.ok:
joke = r.json()
print(joke["joke"])
else:
print("No joke today!")
You can now access the joke for the day:
I am so good at sleeping I can do it with my eyes closed!
2.7 Webhooks
When dealing with APIs, it is sometimes more practical to rely on webhook services than making REST
CALLs. Webhooks are sometimes referred to as reverse API, as they push data automatically from a
service to an application, instead of having the application request data. This approach can be more
elegant when you want to update data as it changes, and this can also be a better way to interact with
an official API, as it can potentially limit your number of CALLs per day.
There are several sites to help you test webhooks, such as the Webhook.site.
P a g e | 28
P a g e | 29
3 EXOS APIs
EXOS offers several APIs for developers that support on-switch automation and external automation.
The following sections describe these options.
P a g e | 30
Parameters:
- cmd: a string containing any valid EXOS CLI command.
- capture: a Boolean, defaulting to False if not specified, returning as a text (string) the CLI
output of the command.
- xml: a Boolean, defaulting to False if not specified, returning the XML that EXOS used to
create the CLI output
- args: a string to provide additional input to some EXOS commands that prompt for more
information
Returns:
- None: if both capture and xml are False
- Captured text: if capture is True
- XML: if xml is True
- Captured text and xml: if both capture and xml are True
Raises:
- RuntimeError: EXOS command is invalid or encountered an error
When you work with JSON data, you can be tempted to use the embedded cli2json.py script.
Calling a script from another script is not supported, as each script has its own session.
This is an example of a simple script:
import exsh
P a g e | 31
if len(sys.argv) < 3:
P a g e | 32
if sys.argv[2] == "down":
exsh.clicmd("delete vlan 42")
exsh.clicmd("config vlan Default add port {}".format(sys.argv[1]))
else:
exsh.clicmd("create vlan 42")
exsh.clicmd("config vlan 42 add port {}".format(sys.argv[1]))
UPM config requires that you create a profile and a log filter to monitor the event you want associated
with this profile.
create upm profile Up_Down_Profile
IF (!$MATCH($EVENT.LOG_COMPONENT_SUBCOMPONENT,vlan.msgs) &&
!$MATCH($EVENT.LOG_EVENT,portLinkStateDown)) THEN
run script upm_port.py $EVENT.LOG_PARAM_0 down
ENDIF
IF (!$MATCH($EVENT.LOG_COMPONENT_SUBCOMPONENT,vlan.msgs) &&
!$MATCH($EVENT.LOG_EVENT,portLinkStateUp)) THEN
run script upm_port.py $EVENT.LOG_PARAM_0 up
ENDIF
.
You can validate the correct execution on the switch as the event happened:
sw1.29 # sh log
06/07/2020 11:03:51.39 <Noti:UPM.Msg.upmMsgExshLaunch> Launched profile
Up_Down_Profile for the event log-message
06/07/2020 11:03:51.38 <Info:vlan.msgs.portLinkStateUp> Port 1 link UP at speed 100
Mbps and full-duplex
06/07/2020 11:03:44.42 <Noti:UPM.Msg.upmMsgExshLaunch> Launched profile
Up_Down_Profile for the event log-message
06/07/2020 11:03:44.42 <Info:vlan.msgs.portLinkStateDown> Port 1 link down
sw1.30 #
P a g e | 33
Execution Information:
3 # enable cli scripting
4 # configure cli mode non-persistent
5 # set var EVENT.NAME LOG_MESSAGE
6 # set var EVENT.LOG_FILTER_NAME "Port_Up_Down"
7 # set var EVENT.LOG_DATE "06/07/2020"
8 # set var EVENT.LOG_TIME "11:03:51.38"
9 # set var EVENT.LOG_COMPONENT_SUBCOMPONENT "vlan.msgs"
10 # set var EVENT.LOG_EVENT "portLinkStateUp"
11 # set var EVENT.LOG_SEVERITY "Info"
12 # set var EVENT.LOG_MESSAGE "Port %0% link UP at speed %1% and %2%"
13 # set var EVENT.LOG_PARAM_0 "1"
14 # set var EVENT.LOG_PARAM_1 "100 Mbps"
15 # set var EVENT.LOG_PARAM_2 "full-duplex"
16 # set var EVENT.LOG_PARAM_3 "1"
17 # set var EVENT.PROFILE Up_Down_Profile
19 # enable cli scripting
21 # IF (!$MATCH($EVENT.LOG_COMPONENT_SUBCOMPONENT,vlan.msgs) &&
!$MATCH($EVENT.LOG_EVENT,portLinkStateDown)) THEN
22 # run script upm_port.py $EVENT.LOG_PARAM_0 down
23 # ENDIF
25 # IF (!$MATCH($EVENT.LOG_COMPONENT_SUBCOMPONENT,vlan.msgs) &&
!$MATCH($EVENT.LOG_EVENT,portLinkStateUp)) THEN
26 # run script upm_port.py $EVENT.LOG_PARAM_0 up
27 # ENDIF
P a g e | 34
If you disconnect from the switch then reconnect as admin, you see the following:
P a g e | 35
sw1.1 # sh log
07/01/2020 00:51:44.26 <Info:System.userComment> User Admin just connected!
07/01/2020 00:51:44.26 <Info:AAA.authPass> Login passed for user admin
through telnet (192.168.56.1)
07/01/2020 00:51:37.20 <Info:AAA.logout> Administrative account (admin)
logout from telnet (192.168.56.1)
07/01/2020 00:51:30.32 <Noti:log.ClrLogMsg> User admin: Cleared the log
messages in memory-buffer.
A total of 4 log messages were displayed.
Use this feature to execute specific scripts or apps based on the user connected to the switch.
ev = throwapi.Subscription("vlan")
P a g e | 36
ev.sub(event_cb)
First verify that this process is present and running, then validate that it is running in the Other CGroup.
P a g e | 37
Your program with the logging capability should look like this:
from exos import api
import exos.api.throwapi as throwapi
import logging
logger = logging.getLogger('test')
logger.setLevel(logging.DEBUG)
logHandler = api.TraceBufferHandler("testbuf", 20480)
logHandler.setLevel(logging.DEBUG)
logHandler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(funcName)s.%(
lineno)s:: %(message)s"))
logger.addHandler(logHandler)
def event_cb(event, subs):
logger.info(event)
ev = throwapi.Subscription("vlan")
ev.sub(event_cb)
You now can access the information when reading the trace buffer of your application:
sw1.50 # create process test python-module test start auto
creating test...
sw1.51 #
sw1.51 # create vlan 10-12
sw1.52 #
sw1.52 # debug ems show trace test testbuf
06/07/2020 14:06:37.002965 [200] <test:testbuf> Begin trace buffer
06/07/2020 14:06:54.479653 [221] <test:testbuf> INFO:test:event_cb.15::
{'meta': {'action': 'create', 'timestamp': 1591538814.48, 'object': 'vlan',
'id': 'exos.vlan.create'}, 'data': {'vr_name': 'VR-Default', 'vlan_name':
'VLAN_0010'}}
06/07/2020 14:06:54.485521 [224] <test:testbuf> INFO:test:event_cb.15::
{'meta': {'action': 'create', 'timestamp': 1591538814.49, 'object': 'vlan',
'id': 'exos.vlan.create'}, 'data': {'vr_name': 'VR-Default', 'vlan_name':
'VLAN_0011'}}
06/07/2020 14:06:54.491825 [227] <test:testbuf> INFO:test:event_cb.15::
{'meta': {'action': 'create', 'timestamp': 1591538814.49, 'object': 'vlan',
'id': 'exos.vlan.create'}, 'data': {'vr_name': 'VR-Default', 'vlan_name':
'VLAN_0012'}}
sw1.54 #
sw1.54 # debug ems show trace test testbuf
P a g e | 38
Parameters:
- cmds: list of strings containing valid EXOS CLI command
- timeout: defaults to 0. This is a synchronous CALL, and the timeout tells the system how long it
should wait for a return
- ignore_errors: Boolean. If set to False, which is the default, execution will stop after the first
failed command
Returns: The output of the command: string
P a g e | 39
Raises:
- CLICommandError(error_msg, cmd): A CLI command returned an error message. The error_msg
attribute is the message received from the CLI and cmd is the command that was being run at
the time
- CLITimeoutError: A CLI request timed out
Note: You are presenting the synchronous CALL in this example, but asynchronous CALLs exist as well.
Enhance your previous example by creating or deleting a VLAN is on the switch:
from exos import api
import exos.api.throwapi as throwapi
import sys
import logging
logger = logging.getLogger('test')
logger.setLevel(logging.DEBUG)
logger.addHandler(logHandler)
def event_cb(event, subs):
meta = event.get('meta')
data = event.get('data')
def main():
# Verify you are running under EXPY. You can't live without it.
if not hasattr(sys, 'expy') or not sys.expy:
print "Must be run within EXPY"
return
P a g e | 40
P a g e | 41
P a g e | 42
To access the Restconf server on a switch, RFC 8040 requires a common URL as the root. The root
resource for EXOS is /rest/restconf/. The datastore is represented by a node named data.
Note: All methods are supported on data.
This example uses self-signed certificates. This is adequate for testing but will generate warning
messages and could potentially result in errors for some applications.
Note: The requests module, and especially urllib3, produces exceptions if you use HTTPS with insecure
certificates. To remove these exceptions, add the following line to the Python class, after you import
urllib3.
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
P a g e | 43
You will also need to add the verify=False parameter to the request CALLs.
EXOS allows you to install customer certificates that have been signed by trusted authorities.
def main():
args = get_params()
if args.username is None:
# prompt for username
args.username = input('Enter remote system username: ')
# also get password
args.password = getpass.getpass('Remote system password: ')
# open a restconf session
rest = Restconf(args.ip, args.username, args.password)
P a g e | 44
vlans = data.get('openconfig-vlan:vlans').get('vlan')
for vlan in vlans:
print("Found VLAN {} with VID {}".format(vlan.get('state').get('name'), v
lan.get('vlan-id')))
main()
As a result, you can see all existing VLANs on the switch:
C:\Extreme API with Python> rest_example.py -i 192.168.56.121 -u admin
Found VLAN Default with VID 1
Found VLAN VLAN_0054 with VID 54
Found VLAN interco with VID 4094
Locating existing VLANs is easy, as it was just a CALL to the root of the VLANs datastore. For
demonstration purposes, you can enhance this example to create a new VLAN and delete an existing
one. To modify the configuration, you must understand the YANG model, used in Openconfig.
Refer to the Restconf documentation, or do a GET (using postman for example) you will see the
following information about VLANs:
{
"openconfig-vlan:vlans": {
"vlan": [
{
"vlan-id": "1",
"state": {
"status": "ACTIVE",
"vlan-id": 1,
"name": "Default",
"tpid": "oc-vlan-types:TPID_0x8100"
},
"config": {
"status": "ACTIVE",
"vlan-id": 1,
"name": "Default",
"tpid": "oc-vlan-types:TPID_0x8100"
}
},
[…]
}
P a g e | 45
Below your endpoint is a VLAN entry which is a list of the VLANs. For each entry in the list, you will see
the element id (in this case, the VLAN id), a state container and a config container.
The Openconfig data model is very consistent, which means that once you understand it, you can easily
access any data: it always follows the same pattern.
To create a VLAN, manipulate the config container following the same structure. To delete a VLAN,
simply point to the endpoint.
from restconf import Restconf
import json
import getpass
import argparse
def list_vlans(rest):
# you make a GET API call for all the vlans
info = rest.get('data/openconfig-vlan:vlans')
data = info.json()
vlans = data.get('openconfig-vlan:vlans').get('vlan')
for vlan in vlans:
print("Found VLAN {} with VID {}".format(vlan.get('state').get('name'), v
lan.get('vlan-id')))
def main():
args = get_params()
P a g e | 46
if args.username is None:
# prompt for username
args.username = input('Enter remote system username: ')
# also get password
args.password = getpass.getpass('Remote system password: ')
main()
P a g e | 47
The result:
C:\Extreme API with Python> rest_example.py -i 192.168.56.121 -u admin
Found VLAN Default with VID 1
Found VLAN VLAN_0054 with VID 54
Found VLAN interco with VID 4094
------------------------------------------
Found VLAN Default with VID 1
Found VLAN H2G2 with VID 42
Found VLAN VLAN_0054 with VID 54
Found VLAN interco with VID 4094
------------------------------------------
Found VLAN Default with VID 1
Found VLAN VLAN_0054 with VID 54
Found VLAN interco with VID 4094
On the switch, you can see the actions have happened, assuming your Python application (from chapter
3.1.2) is still running.
sw1.10 # sh log
06/09/2020 23:17:01.39 <Info:AAA.logout> Administrative account (admin)
logout from app (192.168.56.1)
06/09/2020 23:16:49.58 <Info:System.userComment> Ohoh! VLAN H2G2 has been
deleted
06/09/2020 23:16:41.39 <Info:AAA.authPass> Login passed for user admin
through app (192.168.56.1)
06/09/2020 23:16:30.52 <Noti:log.ClrLogMsg> User admin: Cleared the log
messages in memory-buffer.
To change the configuration of an existing VLAN, use the PATCH HTTP method directly on the endpoint’s
config container to send the modified parameter.
Add the following piece of code to your example:
# you add the vlan again
r = rest.post(url, data)
P a g e | 48
P a g e | 49
The same logic applies to any datastore and allows you to manage switches in a programmatic way,
using an open standard.
P a g e | 50
P a g e | 51
parser.add_argument('-p', '--password',
help='Login password for the remote system',
default='')
args = parser.parse_args()
return args
def main():
args = get_params()
if args.username is None:
# prompt for username
args.username = input('Enter remote system username: ')
# also get password
args.password = getpass.getpass('Remote system password: ')
P a g e | 52
However, the real focus is to work with JSON output, which is easier from a programming perspective.
Note: The JSON output is not documented, you must test your CALLs prior to writing your application.
The following example lists all the VLANs from two switches, and extracts and displays information
about these VLANs. For simplicity, hard code the information about the switches and the CLI command
you want to use.
from jsonrpc import JsonRPC
def main():
vlans = []
for ip in IPS:
# you open a jsonrpc session to the switch
jsonrpc = JsonRPC(ip, USER, PW)
P a g e | 53
sw = {}
sw['ip'] = ip
sw['vlans'] = []
for vlan in response.get('result'):
if vlan.get('status') in ["MORE", "SUCCESS"]:
info = {}
data = vlan.get('vlanProc')
info['ip'] = data.get('ipAddress')
info['netmask'] = data.get('maskForDisplay')
info['name'] = data.get('name1')
info['vid'] = data.get('tag')
sw['vlans'].append(info)
vlans.append(sw)
P a g e | 54
The JSON output is a result of the EXOS CLI command shows the data structures that have been used to
create this display on EXOS. It can be sometimes difficult to find the exact information for a given
feature or protocol parameter.
Note: JSON output is created with the cli2json.py embedded Python script in EXOS. You can use it directly
to see the output for any given command. The output cannot be formatted, so you must create a script to
improve readability in printed output.
Another method is to use the undocumented debug cfgmgr show commands. These commands
directly access the CM objects in the backend. These commands can be very helpful and can be used
with on-switch Python scripting, however their use is not always straightforward, and may require
parameters that are impossible for you to find or to guess.
P a g e | 55
4 VOSS API
VOSS offers a RESTCONF API using the Openconfig model. VOSS powers the VSP product family.
Note: RESTCONF was added to VOSS starting with version 8.0.
When this is done, you can access on-switch documentation for RESTCONF here:
http://<Switch_IP>:8080/apps/restconfdoc/
As with EXOS, both HTTPS and HTTP protocols are supported for RESTCONF on VOSS. To use HTTPS, you
must enable TLS and install a certificate, using the following procedure:
Switch:1>enable
Switch:1#configure terminal
Enter configuration commands, one per line. End with CTRL/Z.
Switch:1(config)#application
Switch:1(config-app)#no restconf enable
Switch:1(config-app)#restconf install-cert-file /intflash/.cert/restconf-
cert.pem
Switch:1(config-app)#restconf tls
Switch:1(config-app)#restconf enable
P a g e | 56
The way to use RESTCONF is identical to that of EXOS, and the endpoints follow the same logic. Be
careful to use the default port used in VOSS for RESTCONF, which is 8080 for HTTP. It must be provided
along with the IP address.
The RESTCONF Python class is available on the Extreme Networks github:
HTTPS://github.com/extremenetworks/ExtremeScripting/blob/master/VOSS/restconf.py
Note: Starting with XMC 8.5, the restconf_voss.py Python class is shipped by default with XMC.
The following example retrieves all existing VLANs on a VOSS switch (or a VM), then adds one and then
deletes it. To better illustrate how similar this process is to EXOS and VOSS, the same code base is
reused, except the data required to create a VLAN on VOSS is modified to add a new parameter.
from restconf_voss import Restconf
import getpass
import argparse
DEFAULT_TCP_PORT = '8080'
def list_vlans(rest):
# you make a GET API call for all the vlans
info = rest.get('data/openconfig-vlan:vlans')
data = info.json()
vlans = data.get('openconfig-vlan:vlans').get('vlan')
for vlan in vlans:
P a g e | 57
def main():
args = get_params()
if args.username is None:
# prompt for username
args.username = input('Enter remote system username: ')
# also get password
args.password = getpass.getpass('Remote system password: ')
P a g e | 58
main()
The result:
C:\Extreme API with Python> rest_voss.py -i 192.168.56.141
Enter remote system username: rwa
Remote system password:
Found VLAN Test with VID 40
Found VLAN Default with VID 1
------------------------------------------
Found VLAN Test with VID 40
Found VLAN H2G2 with VID 42
Found VLAN Default with VID 1
------------------------------------------
Found VLAN Test with VID 40
Found VLAN Default with VID 1
P a g e | 59
5 XMC API
XMC (Extreme Management Center) uses several APIs, and the focus in this section is on the most
recent addition with GraphQL support. This is also referred to as the NBI API (NorthBound Interface),
through the extensive use of the Python capability built into XMC. This API can be accessed either
externally or internally via the Python Scripting Engine.
Note: GraphQL is a query language developed by Facebook, before becoming public in 2015. It accesses
data via HTTP and receives the content formatted in JSON. It is very similar to a REST API but has the
benefit of sending only the information requested, instead of the entire tree. It provides a more efficient
system, which is very appealing when manipulating large databases.
This section is an updated (with XMC 8.4.4) and summary of the document available here:
HTTPS://api.extremenetworks.com/XMC/Scripting/Python_with_XMC_8.1_v0.94.pdf
This is also where jsonrpc.py, restconf.py and restconf_voss.py are located. While Extreme Networks has
an ongoing effort to update the included versions, the timing of releases may prevent their ability to
P a g e | 60
ship the latest revision of the modules. These are available on the Extreme Networks Github, and it is
advisable to update them to the latest version available.
Note: At the time of writing of this document, latest versions are v2.0.0.4 for jsonrpc.py, v1.2.0.0
for restconf.py and v1.0.0.1 for restconf_voss.py.
If identical Python modules are found, the expected precedence is that overrides is used first.
5.1.6.1 emc_vars
When you are writing Python scripts to be run directly from XMC, you can use a global variable named
emc_vars. This variable is a Python dictionary containing all global variables in the system.
It is important to understand this key element. When a Python script must be executed on a device, this
global variable can provide a great deal of useful information about that device, such as IP address,
vendor profile, product family, etc.
With information easily accessible, you can create powerful scripts to run on different products. Another
benefit of XMC is that you do not need to manage device access or store login credentials.
The XMC 8.4.4 list of variables in the emc_vars dictionary is shown below, as returned from this script
executed in XMC:
P a g e | 61
deviceConfigPwd
deviceASN AS number of the selected device
deviceSysOid device system object id
devicePwd login password for the selected device
deviceLogin login user for the selected device
deviceId device DB ID
deviceName DNS name of selected device
deviceVR device virtual router name
deviceSoftwareVer software image version number on the device
deviceType device type of the selected device
deviceIP IP address of the selected device
deviceCliType method used to connect (Telnet/SSH)
deviceEnablePwd
javax.script.name
javax.script.engine_version
javax.script.language
javax.script.filename
javax.script.engine
P a g e | 62
jboss.http.port
jboss.server.log.dir
jboss.bind.address
jboss.bind.address.management
jboss.HTTPS.port
STATUS
USE_IPV6
extreme.hideLegacyDesktopApps
The ports variable returns a string containing all the ports, separated by commas.
5.1.6.2 emc_cli.send()
Another tool provided by XMC is the emc_cli.send() Python object. This object accepts several
parameters. The first parameter is a string containing the CLI command, the second parameter is a
Boolean value that enables you to choose to wait for a system or shell prompt, or not wait. If you set the
Boolean value to False, no CLI output is returned. The Boolean value is optional, and the default is True.
A third (optional) parameter is a timer, in seconds, to wait for information if needed.
There are several ways to use this Python object to retrieve information from CLI command execution:
- isSucces(): Boolean to represent outcome of the last command
- getError(): if it fails, contains the error as a string
- getOutput(): output captured or echoed back from the device (including the CLI command
prompt) as a string
isSuccess()does not indicate whether the CLI command was successful or not, but it does show
whether the send() has been completed correctly. The script handles the result of this CLI command
by analyzing the CLI output.
For example:
# executes a show vlan command and prints the output
cli_results = emc_cli.send("show vlan")
cli_output = cli_results.getOutput()
print cli_output
P a g e | 63
Because the emc_cli object connects to the device using either Telnet or SSH, any device from any
vendor is accessible, however login banners and sub-prompts can vary from one vendor to another.
XMC has a list of CLI rules to access the device.
Starting with XMC 8.1.2, you can customize the CLI rules or the regular expressions for prompt
detection, by creating a file named myCLIRules.xml, located in the same directory as the
CLIRules.xml file (names are case-sensitive).
/usr/local/Extreme_Networks/NetSight/appdata/scripting/
This file should be divided into sections containing regular expressions per vendor, in a similar fashion to
that of the CLIRules.xml file. Typically, BOSS and VOSS access also uses this file.
Note: CLI scripting for BOSS and VOSS is very inconsistent. Devices have a variety of different login
banners and subprompts. Make sure that the CLI profile for a device is correct, as emc_cli relies on the
CLI profile that is set for that device. By default, emc_cli will try to use the regular expressions defined in
CLIRules.xml under the "Avaya" section, but because not all commands and prompts have been
added. As a result, this might be the reason the script fails even if your CLI profile is correct.
When you create the myCLIRules.xml file, the following logic applies when XMC tries to connect to
a device:
- Checks if myCLIRules.xml exists. If it does, use the cliRule name in it.
- Checks if cliRule name exists in CLIRules.xml, if yes use this one.
- Finally, use the default rule name of “*”
The cliRule name normally comes from the device vendor profile. Each device (family, subfamily or
device type) should have a property called cliRuleFileName (this name is misleading, it is really the
cliRuleName, not a file name).
Note: To set the cliRuleName dynamically from Python, invoke emc_cli.setCliRule.
For example:
# must be called before using emc_cli.send
emc_cli.setCliRule("ruleName")
The CLI output returned by emc_cli.send() is a string that contains the CLI command used (first
line).
Note: For XMC 8.0.4 up to XMC 8.1.1, the string returned also included the trailing CLI prompt. XMC 8.1.2
removed it, and XMC 8.1.3 brought it back. You may need to update existing Python scripts.
One way to remove extra lines, which is especially important if you are waiting for JSON-formatted
output, is shown here:
import re
RegexPrompt = re.compile('.*[\?\$%#>]\s?$')
P a g e | 64
You can also find many set methods to use with emc_cli, for example, the session timeout.
# set the session timeout to 80 seconds
emc_cli.setSessionTimeout(80)
P a g e | 65
You can add a description, but the most important part is the user-input variable definition. You must
use the specific meta data shown below to define a variable that the user is prompted to set at when
the script is executed.
#@VariableFieldLabel (description = "Enter Tag Type",
# type = String,
# required = yes,
# validValues = [tag,untag],
# readOnly = no,
# name = "myVar",
# value = "42"
# )
P a g e | 66
P a g e | 67
P a g e | 68
data = r.get('data/openconfig-bgp:bgp/neighbors')
bgp_data = data.json()
if bgp_data:
bgp = bgp_data.get('openconfig-bgp:neighbors').get('neighbor')
print 'Found {} BGP neigbhors'.format(len(bgp))
When you run the script against a BGP router, you will see a result similar to:
Script Name: BGP Test
Date and Time: 2020-06-21T02:20:34.617
XMC User: root
XMC User Domain:
IP: 192.168.56.121
Found 1 BGP neighbors
Received 3 prefixes from 10.0.0.1 in ASN 65002
P a g e | 69
5.2.1 emc_vars
With XMC 8.4.4, the emc_vars Python dictionary returns the following keys:
time
date
userDomain
userName
domain
username
serverVersion
serverIP
serverHTTPSPort
serverName
hostName
isExos
family
vendor
vrName
deviceNosIdName
deviceConfigPwd
devicePwd
deviceName
deviceVR
deviceType
deviceEnablePwd
deviceNosId
deviceASN
deviceSysOid
deviceLogin
deviceSoftwareVer
deviceCliType
deviceIP
devices
workflowPath
workflowStatus
workflowCreatedDateTime
workflowMessage
workflowCategory
P a g e | 70
workflowUpdatedBy
workflowexecutionId
workflowUpdatedDateTime
workflowDescription
workflowTimeout
workflowNosIds
workflowCreatedBy
workflowName
workflowVersion
activityMessage
activityDescription
activityCustomId
activityName
activityNosIds
scriptOwner
scriptName
scriptAssignment
scriptTimeout
scriptType
abort_on_error
javax.script.name
javax.script.engine_version
javax.script.language
javax.script.engine
output
STATUS
auditLogEnabled
failFast
extreme.hideLegacyDesktopApps
status
ports
USE_IPV6
jboss.http.port
jboss.bind.address.management
jboss.server.log.dir
jboss.bind.address
jboss.HTTPS.port
As shown, there are more keys in the emc_vars dictionary. Using a Python script, you can see the
differences between the two environments, using as XMC 8.4.4 as a reference:
There are 46 entries in emc_vars in Scripting Engine
There are 72 entries in emc_vars in Workflow Engine
emc_vars not in Workflow Engine:
Not found: deviceId
Not found: managementPorts
Not found: accessPorts
P a g e | 71
As a result, when you work with Python scripts that could be used in both environments, be careful
when collecting information from emc_vars.
P a g e | 72
Note: You can also select an existing workflow and save it with a different name to use as a template for
a new workflow.
After you have created a new workflow, and entered a name and a description for it, you are ready to
start editing it. Several windows are displayed.
Next to the Menu bar, on the left side, you can see the Workflow List, the Palette, the Designer and
finally the Details window.
From the Workflow List you can select any workflows available on the system. The Palette and the
Designer panels allow you to create the logic of the workflow in a graphical and intuitive way.
From the Palette, select an item to place in the Designer by dragging and dropping the item between the
Start and End buttons. The available items are grouped in categories, depending on their purpose:
- Activities are piece of code or actions that produce something. The Script Activity is most often
used, but other activities are also available.
Script Activity
Shell Activity
P a g e | 73
HTTP Activity
Mail Activity
CLI Activity
Activity Group
- Gateways are objects that allow different execution paths.
Inclusive Parallel
Parallel
- Boundary is a timer object that can be executed if an activity does not complete during a
specified time. This allows you to follow a given path in the Workflow if this happens. The
Workflow Engine checks every 10 seconds and triggers a timer in a range of N to N+10 seconds.
Events allow you to end a path or generate an event when reached.
The Details panel is where you configure these settings.
P a g e | 74
If you select a Python script, the list expands to include the variables shown here:
P a g e | 75
These variables are from your emc_vars dictionary, and some are only significant in some situations,
running with a given activity.
To create a new variable, select the Add button at the top of the Details panel. You can set the default
value, type, and scope.
P a g e | 76
When created, in this example as a string type, the variable becomes accessible from the Python Script
through the emc_vars dictionary.
Here’s a quick example, creating a Python Script in a workflow. You connect the script with the Start and
End gateways, using the arrows from one object to the other, then you can click on the Run button to
execute the workflow, after a save.
P a g e | 77
The output:
Script Name: StefTest_Script_-_4
Date and Time: 2020-06-26T19:32:36.361
XMC User: root
XMC User Domain:
IP:
extreme
Extreme!
Note: If an activity does not need to be run against a device, delete the devices variable so
that the engine will not ask you to provide this input.
5.2.4 emc_results
When you work with Workflows that have Inclusive Parallel gateways, you should provide the outcome
of the action to select the path to follow using the emc_results Python object.
This Python object contains the following methods and functions:
print dir(emc_results)
The output, from XMC 8.4.4:
['DATE_FORMAT', 'DATE_FORMAT_STRING', 'ResultType', 'Status',
'TIMESTAMP_FORMAT', 'TIMESTAMP_FORMAT_STRING', 'TIMESTAMP_FORMAT_STRING_24',
'TIME_FORMAT', 'TIME_FORMAT_STRING', '__class__', '__copy__', '__deepcopy__',
'__delattr__', '__doc__', '__ensure_finalizer__', '__eq__', '__format__',
'__getattribute__', '__hash__', '__init__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__',
'__subclasshook__', '__unicode__', 'addResult', 'batchId', 'childResults',
'class', 'clear', 'deviceIP', 'deviceId', 'elapsedTimeInMillis',
'elaspsedTimeInSecs', 'equals', 'errorMessage', 'fileName', 'get',
'getBatchId', 'getChildResults', 'getClass', 'getDeviceIP', 'getDeviceId',
'getElapsedTimeInMillis', 'getElaspsedTimeInSecs', 'getErrorMessage',
'getFileName', 'getId', 'getMessage', 'getName', 'getOutput',
'getOutputType', 'getResultUrl', 'getStartTime', 'getStatus', 'getStopTime',
'getVariables', 'hasErrors', 'hashCode', 'id', 'isOutputTruncated',
'message', 'name', 'notify', 'notifyAll', 'output', 'outputTruncated',
'outputType', 'put', 'putAll', 'resultUrl', 'setBatchId', 'setDeviceIP',
'setDeviceId', 'setErrorMessage', 'setFileName', 'setId', 'setMessage',
'setName', 'setOutput', 'setOutputTruncated', 'setOutputType',
'setResultUrl', 'setStartTime', 'setStatus', 'setStopTime', 'startTime',
'status', 'stopTime', 'toString', 'variables', 'wait']
P a g e | 78
With this method, you pass an argument variable as a string that you created previously in the activity,
and its value, also as a string. You can test this value with gateways, and other activities. This is the
correct way to change a variable value (if the variable has a scope of workflow).
Note: You can also use this method to pass JSON data, using json.dumps().
emc_results.put("MyVariable", "true")
To better illustrate this, modify your workflow so that you select a path based on the value of a variable.
Delete the link between the script and the “End” gateway by selecting it then selecting on the trash can
icon. Then add an inclusive parallel gateway leading to two new scripts that will end the workflow by
adding another “End” gateway and connecting the scripts to it.
To evaluate the value of MyVariable, first change its scope. In the previous example, this variable was
defined with a scope of Activity. This means its value is not accessible outside of the activity. To check it
with the inclusive parallel gateway, you need to increase its scope by setting it to Workflow.
P a g e | 79
After you have done this, the variable is accessible throughout the workflow. To test it with the inclusive
parallel gateway, select one of the output links and set the condition. This dictates what must be met to
follow this path.
In your example, you defined two paths; one that checks if “My Variable” is equal to “OK”, and the other
to “KO”. The resulting script prints the path has been followed.
P a g e | 80
The first script (Script-4) sets the value to the variable for the rest of the workflow, using
emc_results.put().
print emc_vars['MyVariable']
emc_results.put("MyVariable", "KO")
After you save and run your workflow, you can watch a visual representation of execution of the
workflow. Every step that is completed successfully turns green, while failed steps appear red.
P a g e | 81
Create a new Input by setting a name, a type, the valid values to be entered and the variable that will be
set to this value. You can also choose to make this input required and prompt the user for it.
P a g e | 82
Select a ComboBox with Valid Values of OK and KO. This is what you expect for your inclusive parallel
gateway. Then select the correct variable (My Variable). Your script will no longer modify the variable.
When you run your workflow, you must choose between OK and KO. The path that is followed depends
on your choice.
P a g e | 83
The result:
P a g e | 84
For example, using XMC 8.4.4, the list shown below displays the keys in the emc_vars Python dictionary
when a workflow is triggered by an Alarm, compared to a script in a workflow:
There are 92 entries in emc_vars in Alarm Workflow Engine
There are 72 entries in emc_vars in Workflow Engine
In this example, the Alarm context adds context-specific variables in the emc_vars dictionary. Notice
that there is also a deviceIp variable that appears beside the usual deviceIP variable.
This section describes a simple workflow to show how to configure an Alarm that can execute a specific
workflow, using the new variables.
Start with a regular workflow, and run a Python script when an alarm for a Device Down appears in
XMC. This script is not especially useful, but it illustrates the framework.
The script you run first checks to make sure it is triggered by the Alarms and Events process and that it
can detect specific device types.
# You want to be notified by this workflow only if specific network devices are down
# You could also look at the model type to narrow even further the worflow
P a g e | 85
# This is one way to make sure the workflow is triggered from alarm
# This key is alarm context-specific
if 'alarmName' not in emc_vars:
print "This workflow must be executed within an Alarm"
exit(0)
Next, select the Alarm menu. From the Alarms & Events XMC menu, select the alarm type you want to
modify. You can create a new alarm or edit an existing one, depending on the use case. In this example,
you will edit the existing Device Down alarm.
P a g e | 86
From the Actions tab, add a new Task Action and select your workflow.
P a g e | 87
Select Save. As soon as a Device Down alarm is received by XMC, your workflow is executed. You can
track it from the Workflow Dashboard.
Double-click the workflow to see details and confirm the output of your script.
Script Name: Alarm-Down_Script_-_6
Date and Time: 2020-06-28T15:01:56.478
P a g e | 88
To use the content returned by this URL, in the script following the HTTP activity, define a new variable
and set the “Variable Reference” field to the output of the ID of the HTTP Activity.
Note: This ID can be found in the General tab of an activity, in is the “custom ID” field.
Limit the scope of the new variable to the activity since you will not be using it elsewhere.
P a g e | 89
Your script counts the number of IPs and, based on this, will trigger a different script. To make the
workflow easier to read, enable the link edit mode to you can add a description to each test.
P a g e | 90
The simple Count IP script counts all the IPs in the file received, and removes the comment lines.
Create a second variable called MyVar2 to test for path selection.
received_blacklist = emc_vars['ip_blacklist']
blist = received_blacklist.splitlines()
i = 0
for ip in blist:
if ip.startswith("#"):
P a g e | 91
continue
else:
i += 1
if (i % 2):
emc_results.put("MyVar2", "1")
else:
emc_results.put("MyVar2", "0")
emc_results.put("IPCount", str(i))
The inclusive parallel gateway tests if the MyVar2 variable is equal to 0 or 1.
The final scripts do the same thing, but for the purpose of this example, this is duplicated for each path.
print "There are {} entries in the IP list".format(emc_vars["IPCount"])
Run your workflow to see the output:
P a g e | 92
P a g e | 93
GraphQL is a query language that allows for extremely efficient data transfers where only the necessary
information is transmitted, unlike plain JSON. You can both read and write data from and to the
database. In GraphQL vocabulary, a read is a query and a write is a mutation.
This chapter explores in detail the GraphQL capabilities.
P a g e | 94
5.3.1 emc_nbi
The emc_nbi Python object includes several functions and methods. As of XMC 8.4.4, the example below
shows what is available:
print dir(emc_nbi)
The output is:
['__class__', '__copy__', '__deepcopy__', '__delattr__', '__doc__',
'__ensure_finalizer__', '__eq__', '__format__', '__getattribute__',
'__hash__', '__init__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__str__', '__subclasshook__', '__unicode__',
'class', 'equals', 'example1', 'example2', 'getClass', 'getName', 'hashCode',
'mutation', 'name', 'notify', 'notifyAll', 'query', 'toString', 'wait']
The two methods that you will use the most are query and mutation.
Note: It is possible to use the query method for mutation, but you will use each method for their
respective usage for clarity. However, only the query method supports graphql variables, as presented
here: HTTPS://graphql.org/learn/queries/#variables.
Use query to access the XMC database as read-only, and mutation to edit (write) to the database.
Note: When using mutation, be careful not to corrupt the XMC database. The best practice is to always
have a backup of your database.
P a g e | 95
A query is a read-only operation. This has been supported since XMC 8.1.2 as a beta feature and in 8.2
as GA code.
A query is a string that can be formatted as a JSON object. The most important part of a query are the
fields. Each field is defined in a schema that is dynamically created by the runtime. Some arguments may
be used with some fields.
device(ip: “192.168.1.20”) {
serialNumber
Fields sysContact
}
Argument
The top-level field stipulates a query or a mutation. The sub-systems supported are:
accessControl,
administration,
inventory,
network,
policy,
wireless,
workflows
P a g e | 96
You can access the GraphQL schema description as an IDL file or a JSON file, at the following URLs on an
XMC server:
HTTPS://<xmc-ip-address>:8443/nbi/graphql/schema.idl
HTTPS://<xmc-ip-address>:8443/nbi/graphql/schema.json
Using the GraphiQL interface, you can browse the fields available to us. Let’s have an example and say
you want to retrieve the list of devices managed by XMC, and have their MAC address, firmware version
and site location.
By expanding the Docs panel (on the far right), you have access to all the information available.
Here you can find available fields, field types and sub-fields. Typically, the field type is on the left (in
blue) and the field value is on the right (in orange).
Start at the top with the Query field, then browse the sub-fields to find the Network section.
P a g e | 97
The Device field contains several sub-fields. Build your query in the Network input panel and include the
fields you want to search between curly brackets. Some fields are mandatory, which is indicated by a !.
P a g e | 98
If you run your query from GraphiQL, you will see this (truncated) output from your XMC server:
Note: The list for this device consists of VMs running on a PC, with XMC also running on the same PC
along with a VM. This is a practical and safe way to test queries, workflows, and scripts.
P a g e | 99
This example clearly shows the GraphQL query syntax and the expected output to expect. The output is
formatted in JSON in GraphiQL, but the data is returned as a Java hashmap within a script. This should
be treated as a regular Python dictionary. From here, you have all the tools you need to include NBI
CALLs within your Python scripts and workflows.
Using this same query example in a Python script is easy:
query = '''
{
network {
devices {
baseMac
firmware
sitePath
}
}
}
'''
res = emc_nbi.query(query)
i = 0
j = 0
for item in res['network']['devices']:
if item['sitePath'] == "/World/Extreme Fabric Connect":
i += 1
elif item['sitePath'] == "/World/IP Campus":
j += 1
else:
continue
P a g e | 100
mutation = '''
mutation {
network {
createSite(input: {siteLocation: "%s"}) {
status
message
siteId
}
}
}
''' % SiteName
res = emc_nbi.mutation(mutation)
if res['network']['createSite']['status'] != "SUCCESS":
print "\nCannot create Site {} because {}".format(
SiteName,
res['network']['createSite']['message'])
P a g e | 101
else:
print "\nSuccessfully created Site {}".format(SiteName)
The string starts with the mutation field. Browse the GraphiQL interface to the createSite field. Enter the
required arguments, such as the siteLocation (which is mandatory when creating a new site).
Specify the output you want to receive from this action. Select from the GraphiQL list.
Print the result to display the data returned, and print a comment depending on the outcome.
If you run this script before the new site exists, you can also confirm that the site has been created.
Script Name: NBI Mutation
Date and Time: 2020-06-28T16:00:20.726
XMC User: root
XMC User Domain:
IP: 192.168.56.122
When the site exists, if you run the script again, you will see an error:
Script Name: NBI Mutation
Date and Time: 2020-06-28T16:02:23.390
XMC User: root
XMC User Domain:
IP: 192.168.56.121
P a g e | 102
Scripts, workflow, external NBI CALLs can be limited to specific user groups.
There are two authorization methods for external NBI access:
- basic authorization
- OAuth 2.0.
P a g e | 103
Basic authorization is the standard approach, using the authorization header in HTTP or HTTPS.
However, this simple access method is not very secure, as discussed in chapter 2.3.
The OAuth 2.0 method is recommended when security is an important factor. This method requires you
to create a client in the Client API Access tab in the Administration > Users menu.
This generates a Client ID and a Client Secret that can be used for NBI CALLs only.
Use this Client ID and Client Secret to receive an access token from the oAuth server that can be used to
make valid NBI CALLs.
To receive the access token, initiate a POST to this URL:
HTTPS://<xmc-ip-address>:8443/oauth/token/access-token?grant_type=client_credentials
Include the Content-Type header and set it to application/x-www-form-urlencoded. Set the
authorization header with the Client ID and the Client Secret as password.
The response is a JSON-formatted data with the access_token key containing the value expected.
Additional CALLs use Accept and Content-Type headers set to application/json and Authorization set to
Bearer <access_token>.
P a g e | 104
#!/usr/bin/env python
import json
import requests
from requests import Request, Session
from requests.auth import HTTPBasicAuth
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import argparse
import getpass
def get_params():
parser = argparse.ArgumentParser(prog = 'nbi')
parser.add_argument('-u', '--username',
help='Login username for the remote system')
parser.add_argument('-p', '--password',
help='Login password for the remote system',
default='')
parser.add_argument('-i', '--ip',
help='IP of the XMC 8.1.2+ server')
args = parser.parse_args()
return args
args = get_params()
if args.username is None:
# prompt for username
args.username = input('Enter remote system username: ')
# also get password
args.password = getpass.getpass('Remote system password: ')
if args.ip is None:
#prompt for XMC's IP
args.ip = input('Enter IP of the XMC server: ')
P a g e | 105
session.verify = False
session.timeout = 10
session.auth = (args.username, args.password)
session.headers.update(
{ 'Accept': 'application/json',
'Content-type': 'application/json',
'Cache-Control': 'no-cache',
}
)
if response.status_code != 200:
print('ERROR: HTTP ' + response.reason + '(' + str(response.status_code) + ')
')
else:
# convert JSON string to a data structure
inbound_data = json.loads(response.text)
P a g e | 106
192.168.56.123 IP Campus_08:00:27:C5:83:32
192.168.56.127 sw7
192.168.56.128 sw8
192.168.56.11 192.168.56.11
192.168.56.141 voss01
192.168.56.122 IP Campus_08:00:27:2A:B1:DF
Even if there is a limited number of user workflows, this query returns most of them, including system
workflows.
Reuse your workflow with the “OK/KO” path selection, but this time the you execute the script from an
NBI CALL. In this workflow, initially you only print the path that has been followed. Instead of doing a
print of the message, set workflowMessage to the value you need. This message is displayed on the
Workflow Dashboard when a workflow is successfully executed.
Rewrite the line of code in the output script of OK path to say:
emc_results.put("workflowMessage", "OK path has been followed")
Modify the KO path as well. Now, each time this workflow is successfully executed, it will print the
corresponding message in the message column in the Workflow Dashboard.
Now write a quick script using the NBI to execute this workflow. First find the ID of your workflow, then
execute your workflow, setting MyVariable to either OK or KO. Next, validate the execution of your
workflow and print the result.
The example below uses the Python string replace method to adapt your GraphQL CALLs with your
dynamic values.
P a g e | 107
idQuery = '''
{
workflows {
allWorkflows {
id
name
}
}
}
'''
exeMutation = '''
mutation {
workflows {
startWorkflow (input: { id: <id>
variables: {
MyVariable: "<state>"
}
} ) {
status
errorCode
executionId
message
}
}
}
'''
messageQuery = '''
{
workflows {
execution(executionId: <id>) {
variables
}
}
}
P a g e | 108
'''
WorkflowName = "StefWorkflow"
WorkflowID = 0
Action = "OK"
WAIT = 1
res = emc_nbi.query(idQuery)
sleep(WAIT)
if res['workflows']['startWorkflow']['status'] == "SUCCESS":
execId = res['workflows']['startWorkflow']['executionId']
messageQuery = messageQuery.replace("<id>", str(execId))
info = emc_nbi.query(messageQuery)
print info['workflows']['execution']['variables'].get('workflowMessage')
else:
print res
You must include a wait timer after the execution of your workflow, so that it has enough time to
complete.
When you run your script, you should see the following output:
Script Name: NBI Workflow
Date and Time: 2020-06-29T19:32:46.221
XMC User: root
XMC User Domain:
IP: 192.168.56.126
"OK path has been followed"
P a g e | 109
5.4.1.1 addLocation
This command creates a new location with the specified name.
Parameters:
Name Type Description
locationGroup string Location group name
P a g e | 110
5.4.1.2 addLocationGroup
This function creates a new location group.
Parameters:
Name Type Description
name string Name of new location group
description string Description of location group
5.4.1.3 getAppliances
Retrieve the list of Extreme Appliances.
Returns: A list of Extreme appliances in JSON format.
5.4.1.4 getApplicationBrowserTableData
Retrieve data from the application browser.
Parameters:
Name Type Description
tableId int The table to retrieve the data from, available options are:
0 – appid_attribute (client & server data)
1 – appid_datapoint (application data)
2 – topn_tables
3 – application_usage_default (hourly application data)
4 – application_usage_hr_default (high rate application data)
P a g e | 111
P a g e | 112
5.4.1.5 getBidirectionalFlowsData
Retrieve the latest filtered bidirectional flow data from an Extreme Analytics appliance.
Parameters:
Name Type Description
maxRows int Maximum number of flows to return
searchString string Search string used to query the data
source string Extreme Analytics appliance IP address
5.4.1.6 getLocations
Retrieve the list of location groups and locations.
Returns: A list of location groups and locations in JSON format.
5.4.1.7 getUnidirectionalFlowsData
Retrieve the latest flow data from an Extreme Analytics appliance.
Parameters:
P a g e | 113
5.4.1.8 getVersion
Retrieve Extreme Analytics version.
Returns: Version as a string.
5.4.1.9 importLocationCSV
This creates locations with a provided CSV string.
Parameters:
login = os.environ.get('xmclogin')
passw = os.environ.get('xmcpassw')
url = 'HTTPS://192.168.20.80:8443/axis/services/PurviewWebService/'
P a g e | 114
r = requests.get(url + 'getBidirectionalFlowsData',
verify=False,
auth=(login, passw),
params=payload,
headers=getHeaders)
root = ET.fromstring(r.text)
data = json.loads(root[0].text)
print('json data: ', data)
r = requests.get(url + 'getVersion',
verify=False,
auth=(login, passw),
headers=getHeaders)
root = ET.fromstring(r.text)
print(root[0].text)
The result is:
C:\Extreme API with Python> analytics.py
json data: {'root': [{'pp': '', 'fsip': '192.168.20.83', 'hn': '', 'dl':
'192.168.254.1/VMNIC_0_20.199 (ge.1.3)', 'acm': False, 'tid': -1, 'dt': '', 'du':
4349000874, 'scc': '', 'uk': 38356, 'ic': 14349, 'tapp': False, 'net': -1, 'app': -1,
'ag': 'Protocols', 'an': 'NetBIOS', 'et': 1593629388421, 'rb': 0, 'rc': 14323, 'stos':
'', 're': '', 'fsa': 'student2-o20vd8 (192.168.20.83)', 'ctos': '', 'fc': 14311,
'fsi': 'ge.1.3 [12003 - VMNIC_0_20.199]', 'rp': 0, 'fsm': '00:50:56:bf:11:c2', 'np':
'Pass Through NAC Profile', 'dcc': '', 'fst': 'NetFlow', 'omd':
'HalfSession=2\nuuid=b5df20ee', 'fdip': '192.168.20.56', 'sc': '', 'sl':
'/World/Extreme Networks France', 'ss': '192.168.254.1', 'st': 1589275522843, 'fda':
'192.168.20.56', 'bps': 0, 'aceg': '', 'fdi': 'ge.1.4 [12004 - VMNIC_0_20_198]',
'uav': '192.168.20.83\t192.168.20.56\t137\t17\tNetBIOS', 'l': '/World/Extreme Networks
France', 'ttl': '128 (C)', 'fdm': 'e8:fc:af:e7:3b:34', 'tb': 1632822, 'ct': '', 'r':
'', 'fdp': 'netbios-ns [137]', 'pd': '', 'u': '', 'tsloc': True, 'tloc': True, 'tp':
14323, 'pn': 'UDP', 'dc': ''}, {'pp': '', 'fsip': '192.168.254.161', 'hn': '', 'dl':
'', 'acm': False, 'tid': -1, 'dt': '', 'du': 5839574494, 'scc': '', 'uk': 38357, 'ic':
230146, 'tapp': False, 'net': 1805, 'app': 8540, 'ag': 'Web Applications', 'an':
'netsight.', 'et': 1593629370053, 'rb': 74163096.0, 'rc': 60244, 'stos': '', 're': '',
'fsa': '192.168.254.161', 'ctos': '', 'fc': 60104, 'fsi': 'ge.1.48 [12048]', 'rp':
299986, 'fsm': '00:04:96:9f:d8:b5', 'np': '', 'dcc': '', 'fst': 'NetFlow', 'omd':
'IssuerIdAtCommonName=Netsight
Enterasys\nSignatureAlgorithmId=shaWithRSAEncryption\nSSLVersion=TLS
1.0\nSubjectCount=6\nIssuerIdAtOrganizationName=Enterasys\nValidNotAfter=250111230000Z
P a g e | 115
\nPublicKeySize=2048\nCertificateVersion=v3\nValidNotBefore=150111230000Z\ncommonName=
Netsight.\nTLSServerName=extremecontrol\nIssuerCount=6\nSubjectOrganizationalUnitName=
NetSight
Server\nCertificateLength=798\nuuid=f7d61177\nHalfSession=0\nFlow_HostName=Netsight.\n
ServerIP=192.168.20.80', 'fdip': '192.168.20.80', 'sc': '', 'sl': '/World/Extreme
Networks France', 'ss': '192.168.254.1', 'st': 1590674777472, 'fda': 'xmc
(192.168.20.80)', 'bps': 0.0820000022649765, 'aceg': '', 'fdi': 'ge.1.3 [12003 -
VMNIC_0_20.199]', 'uav': '192.168.254.161\t192.168.20.80\t8443\t6\tnetsight.', 'l':
'', 'ttl': '63', 'fdm': '00:50:56:bf:0f:6b', 'tb': 405628416.0, 'ct': '', 'r': '',
'fdp': 'Alternate HTTPS [8443]', 'pd': '', 'u': '', 'tsloc': True, 'tloc': False,
'tp': 480195, 'pn': 'TCP', 'dc': ''}], 'count': 2}
8.4.4.26
P a g e | 116
6 ExtremeCloud IQ API
ExtremeCloud IQ (XIQ) is a 4th generation cloud-based network management system.
XIQ is entirely API driven internally, and offers an external API (xAPI) as an addition of the internal API to
users. This is a REST API.
The XIQ xAPI is made up of four APIs:
- Identity Management
- Monitoring
- Configuration
- Presence and Location
There are dozens of endpoints to choose from.
Presence and Location require a streaming data service via a webhook.
P a g e | 117
The Redirect URL is necessary regardless of the authentication method you use. XIQ offers two methods:
Bearer Token (basic) and OAuth 2.0 (advanced).
With Bearer Token, the URL you use does not need to be real, but it must use HTTPS, and must match
the URL in the headers of your requests.
Connect to your XIQ account:
HTTPS://extremecloudiq.com
From here, navigate to the Global Settings in the upper right corner.
From the API menu, go to the API Token Management. Click + to create a new API Access token. You
must provide your client ID.
P a g e | 118
By default, the token is valid for 30 days but can be extended up to 365 days.
P a g e | 119
P a g e | 120
Swagger is easy to browse, and lists all URLs and the methods that are supported, including a brief
description. Select an URL to expand it and see what type of data is expected and how the response will
be displayed.
P a g e | 121
6.1.4 Parameters
The information between curly brackets in the URL is optional, with the exception of the ownerId
parameter, which is mandatory. This parameter is the VIQ ID in your XIQ account, in the “About
ExtremeCloud IQ” menu.
P a g e | 122
Nearly every endpoint has a pageSize parameter. By default, XIQ sends data at a maximum of 100
entries at a time, on one return, which can be too small when collecting the clients list, for example. The
pageSize parameter lets you change this value to better match your needs.
The pagination entry in the data returned provides valuable information.
'pagination': {
'offset': 0,
'countInPage': 7,
'totalCount': 7
}
In this example, you know that there are 7 objects and that you received 7, and you have received
everything on page 0, so you don’t need more pages to display the information.
If you need more than 100 objects, you must set the pageSize accordingly, or request the page numbers
using the page parameter.
P a g e | 123
The clientId or deviceId optional parameters are unique IDs for each client or device. They are found
when listing the appropriate data, and when they are added to the endpoint, where they point to more
information.
baseURL = "HTTPS://ie.extremecloudiq.com/"
clientSecret = os.environ.get('clientSecret')
clientId = os.environ.get('clientId')
redirectURI = 'HTTPS://foo.com'
authToken = os.environ.get('authToken')
ownerID = os.environ.get('ownerID')
try:
r = requests.get(baseURL + 'xapi/v1/monitor/devices', headers=requestHeaders, params=params)
if r.status_code != 200:
print("Connection failure! Unable to connect to API")
sys.exit(1)
P a g e | 124
except requests.exceptions.RequestException as e:
print("There was an error accessing XIQ API")
sys.exit(1)
data = r.json()
EXOSList = []
VSPList = []
for device in data.get('data'):
entry = {}
if device.get('simType') == "SIMULATED":
continue
entry['model'] = device.get('model')
entry['ip'] = device.get('ip')
entry['firmware'] = device.get('osVersion')
entry['deviceID'] = device.get('deviceId')
entry['connected'] = device.get('connected')
if device.get('model').startswith("X"):
EXOSList.append(entry)
elif device.get('model').startswith("VSP"):
VSPList.append(entry)
if len(EXOSList):
print("\nEXOS switches:")
for exos in EXOSList:
print('\t{} with IP {} running EXOS version {}'.format(exos['model'],
exos['ip'],
exos['firmware']))
try:
r = requests.get(baseURL + 'xapi/v1/monitor/devices/{}'.format(exos['deviceID']),
headers=requestHeaders,
params=params)
if r.status_code != 200:
print("Connection failure! Unable to connect to API")
sys.exit(1)
P a g e | 125
except requests.exceptions.RequestException as e:
print("There was an error accessing XIQ API")
sys.exit(1)
data = r.json()
up = 0
down = 0
for port in data.get('data').get('ports'):
if port.get('status') == "UP":
up += 1
elif port.get('status') == "DOWN":
down += 1
print("\t{} ports UP and {} ports DOWN".format(up, down))
if len(VSPList):
print("\nVSP switches:")
for vsp in VSPList:
print('\t{} with IP {} running VOSS version {}'.format(vsp['model'],
vsp['ip'],
vsp['firmware']))
EXOS switches:
X460_G2_24p_10_G4 with IP 192.168.254.160 running EXOS version
30.6.1.11 patch1-11
2 ports UP and 33 ports DOWN
P a g e | 126
baseURL = "HTTPS://ie.extremecloudiq.com/"
clientSecret = os.environ.get('clientSecret')
clientId = os.environ.get('clientId')
redirectURI = 'HTTPS://foo.com'
authToken = os.environ.get('authToken')
ownerID = os.environ.get('ownerID')
POLICY = "PolicyAPI"
PolicyID = 0
DeviceID = 0
SN = "1441N-41334"
assignPolicy = {"sns": ["1441N-41334"]}
def restGet(url):
try:
r = requests.get(baseURL + url, headers=requestHeaders, params=params)
if r.status_code != 200:
print("Connection failure! Unable to connect to API")
P a g e | 127
sys.exit(1)
except requests.exceptions.RequestException as e:
print("There was an error accessing XIQ API")
sys.exit(1)
return r.json()
data = restGet('xapi/v1/configuration/networkpolicy/policies')
for policy in data.get('data'):
if policy['name'] == POLICY:
PolicyID = policy['id']
break
data = restGet('xapi/v1/monitor/devices')
for device in data.get('data'):
if device['serialId'] == SN:
DeviceID = device['deviceId']
break
assignPolicy['deviceIds'] = [DeviceID]
try:
r = requests.post(baseURL + 'xapi/v1/configuration/networkpolicy/{}/devices'.form
at(PolicyID), headers=postHeaders, params=params, json=assignPolicy)
if r.status_code != 200:
print("Connection failure! error code: {}".format(r.status_code))
sys.exit(1)
except requests.exceptions.RequestException as e:
print("There was an error accessing XIQ API")
sys.exit(1)
P a g e | 128
print(r.json())
Run this script on your test XIQ account. The following information is returned:
C:\Extreme API with Python> xiq2.py
{'data': {'status': 200, 'successMessage': 'Network Policy 382247794480476
applied to devices successfully.'}}
You have now successfully assigned a network policy to your EXOS switch.
baseURL = "HTTPS://ie.extremecloudiq.com/"
clientSecret = os.environ.get('clientSecret')
clientId = os.environ.get('clientId')
redirectURI = 'HTTPS://foo.com'
authToken = os.environ.get('authToken')
ownerID = os.environ.get('ownerID')
P a g e | 129
'X-AH-API-CLIENT-ID': clientId,
'X-AH-API-CLIENT-REDIRECT-URI': 'HTTPS://foo.com',
'Authorization': authToken,
'Accept': 'application/json',
'Content-type': 'application/json'
}
webhookurl = 'HTTPS://webhook.site/5b8f683d-e2e5-4373-aea8-9149a762357d'
try:
r = requests.post(baseURL + 'xapi/v1/configuration/webhooks', headers=subscriptio
nHeaders, data=params)
if r.status_code != 200:
print("Connection failure! Unable to connect to API. error code: {}".format(r
.status_code))
sys.exit(1)
except requests.exceptions.RequestException as e:
print("There was an error accessing XIQ API")
sys.exit(1)
print(r.text)
After you execute this code, you should see the following result:
C:\Extreme API with Python> webhook.py
{"data":{"ownerId":88999,"application":"WebhookTest","secret":"test","url":"H
TTPS://webhook.site/5b8f683d-e2e5-4373-aea8-
9149a762357d","messageType":"LOCATION_AP_CENTRIC","createdAt":"2020-07-
03T07:22:46.230Z","id":382247794480746}}
If you connect to your XIQ account, you can see the webhook is configured from the Global Settings
menu, in the API Data Management panel.
P a g e | 130
Enable Presence Analytics in your network policy in the Additional Settings panel.
P a g e | 131
To stop the webhook, send a DELETE to the API endpoint, and specify the correct subscription ID. You
can also list and modify all your webhooks.
This example shows how to list your webhooks:
import requests
import os
import sys
baseURL = "HTTPS://ie.extremecloudiq.com/"
clientSecret = os.environ.get('clientSecret')
clientId = os.environ.get('clientId')
redirectURI = 'HTTPS://foo.com'
authToken = os.environ.get('authToken')
ownerID = os.environ.get('ownerID')
P a g e | 132
def restGet(url):
try:
r = requests.get(baseURL + url, headers=requestHeaders, params=params)
if r.status_code != 200:
print("Connection failure! Unable to connect to API")
print(r.content)
sys.exit(1)
except requests.exceptions.RequestException as e:
print("There was an error accessing XIQ API")
sys.exit(1)
return r.json()
data = restGet('xapi/v1/configuration/webhooks')
print(data)
The result should match your current configuration:
C:\Extreme API with Python> webhookget.py
{'data': [{'ownerId': 88999, 'application': 'WebhookTest', 'secret': 'test',
'url': 'HTTPS://webhook.site/5b8f683d-e2e5-4373-aea8-9149a762357d',
'messageType': 'LOCATION_AP_CENTRIC', 'createdAt': '2020-07-
03T07:22:46.230Z', 'id': 382247794480746}], 'pagination': {'offset': 0,
'countInPage': 1, 'totalCount': 1}}
Stop the webhook by adding the following code at the end of your previous example:
subID = 0
for webhook in data.get('data'):
if webhook.get('application') == "WebhookTest":
subID = webhook.get('id')
break
if subID:
r = requests.delete(baseURL + 'xapi/v1/configuration/webhooks/{}'.format(subID),
headers=requestHeaders, params=params)
print(r.status_code)
print(r.text)
P a g e | 133
Validate that the webhook subscription is no longer part of your XIQ configuration.
P a g e | 134
Add the scope entry in the body, to specify a given scope for this access.
As of version 5.06, the following scopes are available:
P a g e | 135
"scopes" : {
"site" : "RW",
"network" : "RW",
"deviceAp" : "RW",
"deviceSwitch" : "RW",
"eGuest" : "RW",
"adoption" : "RW",
"troubleshoot" : "RW",
"onboardAaa" : "RW",
"onboardCp" : "RW",
"onboardGroupsAndRules" : "RW",
"onboardGuestCp" : "RW",
"platform" : "RW",
"account" : "RW",
"application" : "RW",
"license" : "RW",
"cliSupport" : "RW"
}
In return, the Extreme Campus Controller server issues a token. This is the Bearer token that you will use
for REST API CALLs. The token has a finite lifetime that defaults to 7200 seconds (which is 2 hours).
Depending on the configured user privileges, the adminRole is either FULL, allowing access in read-write
(RW) to everything, or read (R), granting read-only access.
This information is part of the response from the Extreme Campus Controller server. For example:
import os
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
xcclogin = os.environ.get('xcclogin')
xccpassw = os.environ.get('xccpassw')
auth_url = 'HTTPS://192.168.20.90:5825/management/v1/oauth2/token'
auth_body = {'grantType': 'password', 'userId': xcclogin, 'password': xccpassw}
print(r.text)
The response:
P a g e | 136
When you have the Bearer token, you can add it to the Authorization header along with the Accept and
Content-Type headers, set to application/json.
In the documentation, locate the endpoint you want to use to access, modify, create, or delete
information on the Extreme Campus Controller server.
P a g e | 137
xcclogin = os.environ.get('xcclogin')
xccpassw = os.environ.get('xccpassw')
baseURL = 'HTTPS://192.168.20.90:5825'
auth_url = baseURL + '/management/v1/oauth2/token'
auth_body = {'grantType': 'password', 'userId': xcclogin, 'password': xccpassw}
myHeaders = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + bearerToken
}
P a g e | 138
def restGet(endpoint):
try:
r = requests.get(baseURL + endpoint, verify=False, headers=myHeaders, timeout=5)
except requests.exceptions.Timeout:
print("Timeout!")
return None
if r.status_code != 200:
print("Cannot access XCC REST API! Error code: {}".format(r.status_code))
print(r.content)
return None
return r.json()
data = restGet('/management/v1/aps')
if data:
print(len(data))
Executing the script results in the following:
C:\Extreme API with Python> xcc.py
35
You can also modify your code to count the number of AP models.
data = restGet('/management/v1/aps')
if data:
APList = []
print(len(data))
for ap in data:
APList.append(ap.get('platformName'))
P a g e | 139
AP3935 3
AP410 3
AP510 3
AP3915 4
AP3916 4
AP505 4
SA201 8
new_list = []
for count, model in sorted(((APList).count(ap), ap) for ap in set(APList)):
entry = {}
entry['model'] = model
entry['count'] = count
new_list.append(entry)
print(new_list)
data = restGet('/management/v3/roles')
if data:
print("\nThere are {} roles".format(len(data)))
found = False
for name in data:
if name.get('name') == "Stef":
print("The role Stef already exists")
found = True
P a g e | 140
break
if not found:
role = {'name': 'Stef', 'defaultAction': 'allow', 'defaultCos': None}
r = requests.post(baseURL + '/management/v3/roles', verify=False,
headers=myHeaders, json=role)
if r.status_code != 201:
print("Cannot access XCC REST API! Error code: {}".format(r.status_code))
print(r.content)
exit(0)
The output should look like this:
C:\Extreme API with Python> xcc.py
There are 35 APs
[{'model': 'AP310', 'count': 1}, {'model': 'AP3917', 'count': 1}, {'model':
'AP460', 'count': 1}, {'model': 'AP3912', 'count': 3}, {'model': 'AP3935',
'count': 3}, {'model': 'AP410', 'count': 3}, {'model': 'AP510', 'count': 3},
{'model': 'AP3915', 'count': 4}, {'model': 'AP3916', 'count': 4}, {'model':
'AP505', 'count': 4}, {'model': 'SA201', 'count': 8}]
Log in to your Extreme Campus Controller server to confirm that you have one more role (43) and the
role you created is listed.
P a g e | 141
P a g e | 142
if r.status_code != 201:
print("Cannot access XCC REST API! Error code: {}".format(r.status_code))
print(r.content)
exit(0)
else:
role['name'] = "H2G2"
r = requests.put(baseURL + '/management/v3/roles/{}'.format(role['id']),
verify=False, headers=myHeaders, json=role)
if r.status_code != 200:
print("Cannot access XCC REST API! Error code: {}".format(r.status_code))
print(r.content)
else:
print("Role name changed")
The result:
C:\Extreme API with Python> xcc.py
There are 35 APs
[{'model': 'AP310', 'count': 1}, {'model': 'AP3917', 'count': 1}, {'model':
'AP460', 'count': 1}, {'model': 'AP3912', 'count': 3}, {'model': 'AP3935',
'count': 3}, {'model': 'AP410', 'count': 3}, {'model': 'AP510', 'count': 3},
{'model': 'AP3915', 'count': 4}, {'model': 'AP3916', 'count': 4}, {'model':
'AP505', 'count': 4}, {'model': 'SA201', 'count': 8}]
There are 43 roles
The role Stef already exists
Role name changed
In Extreme Campus Controller, you can confirm that the role name has been changed.
P a g e | 143
P a g e | 144
Because you executed the entire code, it created Stef again because Stef no longer existed after you
renamed it to H2G2. The new piece of code found and deleted each new entry. Because this was a
successful delete, no response was returned.