WAE Design API

This is the documentation for the WAE Design API.

The WAE Design API exposes functionality and objects used by the Design product suite. It does that via an RPC mechanism, handled by the WAE Design API Service. The WAE Design API can be used to write programs that interact with this service. The supported languages are currently Java and Python.

This documentation was generated from a C++ version of the API by Doxygen. The reader can readily translate it to the specific target language as follows:

C++ Java Python
namespace package module
std::vector native array native list (exception: a sequence of bytes,
such as ByteList, maps to a string)
std::map java.util.Map native dictionary
struct class with public data members class with corresponding attributes

Python Setup

Most of the examples in this documentation are Python examples. In order to run them, you need a proper Python setup. In all the cases, you need a 64-bit version of Python installed.

The design_api_python Utility

The simplest way to run a WAE Design API Python script is to use the design_api_python tool. It will set the correct environment and call the correct Python interpreter for your system. (Instructions for installing the required Python interpreter are given in the platform-dependent secsions below.)

$CARIDEN_HOME/bin/design_api_python my-python-script.py

Using the WAE Design API in Add-Ons

When creating WAE Design add-ons that use this API, we strongly recommend using design_api_python, since otherwise you wouldn't be able to run your add-on on all platforms.

Here's a simple example: we'll create an add-on that loads the given plan file and saves it back unmodified. The addon.txt file looks like this:

<AddOnConfigs>
Property Value
Name API Null Add-On
Description Duplicates the plan file
Run design_api_python NullAPI.py

And here is the add-on script, NullAPI.py:

# We kept it simple here. One could use the Python optparse/argparse
# libraries instead.
import sys
import os
import os.path
plan_file = sys.argv[sys.argv.index('-plan-file')+1]
out_file = sys.argv[sys.argv.index('-out-file')+1]
# Do nothing: load a plan file, save it back.
print(("Loading file "+plan_file+"..."))
plan = conn.getPlanManager().newPlanFromFileSystem(plan_file)
# local directory to put the output files.
resultDir = os.environ.get('CARIDEN_QA_RESULTSDIR')
if resultDir:
out_file = os.path.join(resultDir, out_file)
print(("Saving file "+out_file+"..."))
plan.serializeToFileSystem(out_file)
logFile = conn.getLogFileName()
if logFile and os.path.isfile(logFile): os.remove(logFile)

Linux Python Setup

The Linux version of the WAE Design API has support for both python 2 and python 3.

Python 3 version of Linux WAE Design API requires Python 3.6. If you are not using design_api_python, then the environment variables you need to set in order to use the WAE Design API in Python scripts are:

export CARIDEN_HOME=/my/installation/home # Replace by actual folder
export PYTHONPATH=$PYTHONPATH:$CARIDEN_HOME/lib/python3
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CARIDEN_HOME/lib:$CARIDEN_HOME/lib/python3

Python 2 version of the WAE Design API requires Python 2.6. If you are not using design_api_python, then the environment variables you need to set in order to use the WAE Design API in Python scripts are:

export CARIDEN_HOME=/my/installation/home # Replace by actual folder
export PYTHONPATH=$PYTHONPATH:$CARIDEN_HOME/lib/python
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CARIDEN_HOME/lib

Windows Python Setup

Make sure you have the 64-bit version of Python 2.7 installed from here.

If you are not using design_api_python, then the environment variables you need to set in order to use the WAE Design API in Python scripts are:

set CARIDEN_HOME=\my\installation\home
set PYTHONPATH=%CARIDEN_HOME%\lib\python2.7
set PATH=%PATH%;%CARIDEN_HOME%\lib\exec

OSX Python Setup

OSX already comes with Python installed. If you are not using design_api_python, then the environment variables you need to set in order to use the WAE Design API in Python scripts are:

export CARIDEN_HOME=/my/installation/home # Replace by actual folder
export PYTHONPATH=$PYTHONPATH:$CARIDEN_HOME/lib/python2.6
export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$CARIDEN_HOME/lib

Getting Started

To start using the WAE Design API, the first thing you need to do is to establish a connection between your program and a running instance of a WAE Design API Service. The WAE Design API Service is a daemon that listens to WAE Design API RPC requests and processes them, managing objects, and performing operations such as simulation and optimization. Multiple clients can connect to the same service, from different machines and processes.

You can start a service instance manually by executing $CARIDEN_HOME/bin/designapid, but for simple programs the easiest way is to use utility functions that let you start the service from the program itself, and have it closed automatically when the program exits. In Python:

Here we used the ServiceConnectionManager to create a local service instance and return the resulting connection (in a ServiceConnection instance). In this particular case, the service instance will exit when the script terminates.

Note
In this introduction we will not go into setting up and working in an RPC environment. We will assume that we will always use the convenience methods above and work with local, temporary services that terminate when the scripts exit.

Let us now load a plan file into the service instance, and print the names of all the nodes in the file:

plan = conn.getPlanManager().newPlanFromFileSystem(sys.argv[1])
for nodeKey, node in plan.getNetwork().getNodeManager().getAllNodes().items():
print((node.getName()))

Most of the code should be self explanatory, but a few concepts are worth noticing:

  • To get the list the nodes from the plan, we had to go through a network and a node manager. A network is the section of the plan file that contains the network elements (including nodes), the network topology, and other related information. The node manager is the object that manages the nodes in that network.

Basic Concepts

The MAPE API exposes a large number of objects and functionality, but is designed around a small number of concepts that are used consistently throughout the system.

Objects, Attributes, and Managers

At the highest level, objects represent some entity via a set of attributes (also called properties), and the operations that can be performed on those attributes. A Node object, for example, can represent a network router via attributes such as name, ip address, and so on.

Attributes can represent a single value or a collection of values. We call these simple and collection attributes, respectively. In our Node example, we can have an interfaces collection attribute containing the list of all interfaces in the node.

In the WAE Design API, objects are managed by a respective object manager. Nodes, for example, are managed by the network's NodeManager. The manager is responsible for creating, removing, and accessing objects, while keeping the object model consistent - removing a node, for example, results in all the node interfaces being removed too.

Simple Attributes

There are only two operations that can be performed on simple attributes: reading their value, and setting their value. Objects expose those via methods that are always named as follows:

  • getAttributeName: Get, or read, the value of the attribute. Read operations always succeed - they never throw an exception, for example.
  • setAttributeName: Set, or write, a new value for the attribute. This operation can fail (example: trying to set a negative TE metric on an interface), and will throw a suitable exception in that case. Not all attributes in an object are writeable - those that are not are called read-only attributes.

Collection Attributes

Collection attributes can be thought of as simple attributes whose value type is a collection, or a list of elements. The RPC nature of our API, however, requires us to duplicate some of the collection methods in the object itself.

Here's an example: suppose you want to find out whether a node contains a certain tag. In a non-RPC environment, the natural way of doing this would be something like this:

if "mytag" in node.getAllTags():
doSomething()

In a non-RPC environment, the getAllTags call can be made almost free by making it an inline method that returns a reference to an existing list. In an RPC environment, however, the server has to create and serialize the list, send it over the wire, and the client has to then reconstruct the list locally. If all one wants to do is to check for the existence of a particular element, this can be too expensive. For this reason, we provide extra operations for collection attributes.

For naming the operations, we need to know two names (as opposed to just the attribute name for simple attributes):

  • The ElementName is the name of the element contained in the collection. For a list containing interfaces, this could be Interface.
  • The ListName is the name of the list (and the attribute) itself - usually the plural of ElementName. A list of interfaces could be called Interfaces.

Finally, the collection can be either a list collection or a map collection. The rule of thumb is that if the collection contains objects, then it will be a map from the object keys to the objects themselves. If the collection contains something else, then the collection will be a list. The API differences will be described below.

We are now ready to describe the read operations:

  • getAllListName(): This returns the complete list of elements. For map collections, what is returned is a map from key to element. This method always succeeds.
  • getAllElementNameKeys: This method is for map collections only, and it returns the list of all the keys in the collection.
  • getElementName(key): Get the element for a given key (for map collections) or index (for list collections). It always succeeds, which means that it must sometimes return an "optional value" (which we will discuss later).
  • getListName(key list): Get a map from keys (or indices) to elements. This method always succeeds.
  • hasElementName(key/value): For map collections, check whether an element exists for the give key. For list collections, check that the list contains at least one element with the given value.

And the write operations will be:

  • addElementName(value): (For list collections only.) Add an element to the list. Ordering is not guaranteed.
  • setElementName(key,value): Set the value for a given key (map collections) or index (list collections).
  • removeListName(key list): Remove a list of elements from a map collection.
  • removeElementName(key): Remove a single element from a collection.
  • removeAllListName(): Remove all elements from the collection.

Note that the methods above are a superset of what will be available for any given collection attribute. We may chose to provide, for small collections for exemple, a small subset of those.

Optional Attributes

Some simple attributes are also optional, that is, they are allowed to have an "unset" value (we can also call them undefined). A well-behaved program must not assume that an optional data member or parameter always has a value.

We use the built-in optional value support that our RPC framework provides:

  • In Python, the unset value for any data type is represented as Ice.Unset. The way to check for an unset value is to use Python's is operator:
    if value is Ice.Unset:
    print('Value is not set.')
  • Java, an Ice.Optional type wraps the optional value and provides methods for examining and changing its value. The key methods are get, set, isSet, and clear.

Object Keys

Objects are uniquely identified within a collection by keys. The key for node objects, for example, is their name. An object's key is defined by a corresponding key structure, that can be obtained via a call to the object's getKey() method.

Keys are the way to retrieve objects from their respective manager. If you want to "find a node," for example, you build the respective NodeKey and query the plan's NodeManager:

import os
import os.path
# Create a service to be used by this script
# Load a sample file from the MATE distribution
fileName = os.path.join(os.environ['CARIDEN_HOME'],'samples','euro4.txt')
plan = conn.getPlanManager().newPlanFromFileSystem(fileName)
# Check for the existence of a few nodes
for nodeName in [ 'cr1.ams', 'cr7.ams', 'er1.lon', 'er1.nyc' ]:
nodeKey = com.cisco.wae.design.model.net.NodeKey(name = nodeName)
if plan.getNetwork().getNodeManager().hasNode(nodeKey):
print(('Network contains node '+nodeName))
else:
print(('Network does not contain node '+nodeName))
logFile = conn.getLogFileName()
if logFile and os.path.isfile(logFile): os.remove(logFile)

Records

Records are structures containing the attributes of an object. You can think of a record as a serialization, in native data types, of an object's attributes. (For those used to the WAE Design "table API," a record could be thought of as one row of the respective object table.)

Records are the primary way of specifying an object's attributes at the time of creation.

In an RPC environment, especially, records play a very important role. Let's briefly compare records with objects:

  • In the client, an object is really a proxy to the real object, that resides on the server. Every operation on that object requires an RPC call over the wire. Modifications to that object are subject to consistency enforcement - for example, you can't use an existing node name as the name for another node.
  • A record is an independent client-side representation of an object. Everything that is done to a record is a local operation. The client can modify the record at will, without worrying about consistency.

When performing bulk filtering, for example (we'll have examples of that below), it is much more efficient to first get a list of records, and then filter based on those, than to loop through objects and requesting their attributes on-the-fly.

Filtering

Users of the WAE Design UI are used to perform complex filtering operations easily from context menus and the table filter dialog box. These operations can range from "filter to interfaces with metric greater than 7" to "filter to demands through this interface."

We currently do not provide any advanced filtering options in the WAE Design API, but a lot can be done by using records and, in the case of Python, list comprehensions. We plan on adding specialized functionality in the future for common and performance-sensitive cases, but in the meantime here are a few examples on how to achieve some filtering tasks:

This snippet finds the interfaces with TE metric greater than a given number, and shows a common pattern when working with "all records" to filter in Python - we use a list comprehension to get the key for values that satisfy a given condition:

ifaceManager = plan.getNetwork().getInterfaceManager()
ifaceKeys = [ InterfaceKey(v.sourceKey, v.name) for v in ifaceManager.getAllInterfaceRecords()
if v.teMetric > 7 ]

Before we give examples that require a simulation, let us set up those:

simManager = conn.getSimulationManager()
routeSim = simManager.newRouteSimulation(plan, FailureScenarioRecord())
sim = simManager.newTrafficSimulation(
routeSim,
plan.getNetwork().getTrafficLevelManager().
getTrafficLevel(TrafficLevelKey(name='Default')),
None)

Let us now find all interfaces with an utilization greater than a given value. We use the same pattern we used before, except now we deal with interface traffic simulation records:

ifaceSimTraffRecords = sim.getAllInterfaceSimulatedTrafficRecords()
ifaceKeys = [ k for k,v in ifaceSimTraffRecords.items()
if v.utilSim > 0.5 ]

Something a little more complicated now: find the interface with maximum utilization. In this case, we still use the records, but pass them to the Python max function. The key parameter lets us specify the value to use to compute the maximum.

maxUtilIfaceKey = max(ifaceSimTraffRecords,
key=lambda k: ifaceSimTraffRecords[k].utilSim)

We could also find the, say, five most utilized interfaces by using Python's heapq.nlargest:

import heapq
ifaceKeys = heapq.nlargest(5, ifaceSimTraffRecords,
key=lambda k: ifaceSimTraffRecords[k].utilSim)

Finally, let us find all the demands going through the maximum utilization interface. We use the same list comprehension pattern, now with demand route records. Since a demand "goes through an interface" even if it is inside an LSP, we ask the simulator to expand the routes for us. (Note that we don't need a traffic simulation here - if you knew the interface in advance, all you'd need would be a route simulation.)

routeRecords = routeSim.getAllDemandRouteRecords(RouteOptions(expand = True))
demands = [ k for k,v in routeRecords.items()
if maxUtilIfaceKey in v.interfaceUsage ]

Simulating

There are two types of simulations supported by the WAE Design API:

  • Route simulations answer the basic question "how is this entity routed?," where the "entity" can be, for example, a demand or an LSP. This answer is computed by a RouteSimulation for a given plan under a given failure scenario, and given in the form of a RouteRecord.
  • Traffic simulations answer the basic question "how much traffic is going through this network element?" This is built by adding the plan's demands on top of a route simulation. The answer is given by a TrafficSimulation in the form of various "simulated traffic records."

Simulations are created from a simulation manager.

Route Records

Route simulations represent routes via RouteRecords, as mentioned above. The representation deserves a bit of explanation:

ECMP route representation is a complex subject, and the ideal structure is highly dependent on the particular use case. Our basic RouteRecord structure, is a lightweight structure that makes it easy to find out what is the percentage of the route going through an interface or LSP. It is thus tailor-made to make it easy to perform traffic and similar calculations.

With the route record we also provide, optionally, some path properties that are both commonly used, and hard to compute from the information given. These include the path latency (maximum, minimum, and average) and the smallest ECMP split along the path.

As the API develops, we will be adding more convenient structures and methods to satisfy use cases that are not readily dealt with the current functionality.