Data Access Layer

In EXOS, the Data Access Layer is implemented by CM. CM has frontends and backends.

  • A frontend is a process that consumes data, such as a user interface.
  • A backend is a process that owns and produces data, such as a protocol daemon. Backends are also commonly referred to as modules.

At the lowest level, a frontend will construct a request in the form of an XML document. The request will then be routed to the appropriate backend. The backend responds with another XML document.

The EXOS Python API provides a high-level abstraction layer for both the frontend and backend. Python applications need not construct the XML documents directly.

CM Frontend Overview

The cmfrontend module implements PEP-249, the Python Database API Specification. This section assumes familiarity with PEP-249. Below is an overview of the module’s functionality.

To initialize the frontend, use the connect() function. The single parameter is a username, which is informational:

from exos.api import cmfrontend as dbapi
cm_conn = dbapi.connect("admin")

With a connection, create a cursor:

cursor = cm_conn.cursor()

A cursor can be used to execute many operations. In the CM frontend, we use a Request object to represent an operation.

In the following example, fields from the singleton dm_system object are retrieved. A Request instance is created and the fields are added by calling add_field(). In each call, the first parameter is an arbitrary string identifying the field for the user. The second parameter identifies a column within CM. Columns are identified by (module, table, column). Finally, the Request is executed and the resulting fields are fetched:

req = dbapi.request()
req.add_field("name",     dbapi.Column(("dm", "dm_system"), "sysName"))
req.add_field("location", dbapi.Column(("dm", "dm_system"), "sysLocation"))
req.add_field("contact",  dbapi.Column(("dm", "dm_system"), "sysContact"))

cursor.execute(req)

fields=cursor.fetchone()

The fetchone() method is defined by PEP-249. Additionally, the cmfrontend module provides the fetchrow() method, which provides a richer interface into the data returned by CM:

cursor.execute(req)
row=cursor.fetchrow()

fields=row.field_values
rowid=row.rowid
msg=row.opMsg

If the Request will retrieve multiple rows, an index column must be provided:

req = dbapi.request()
req.add_index(dbapi.Column(("epm", "epmsyscpuinfo"), "slotid"))
req.add_field("name",        dbapi.Column(("epm", "epmsyscpuinfo"), "slotname"))
req.add_field("util_5_secs", dbapi.Column(("epm", "epmsyscpuinfo"), "kernel_cpu_5_secs"))
req.add_field("util_1_min",  dbapi.Column(("epm", "epmsyscpuinfo"), "kernel_cpu_1_min"))
req.add_field("util_60_min", dbapi.Column(("epm", "epmsyscpuinfo"), "kernel_cpu_60_min"))
req.add_field("util_max",    dbapi.Column(("epm", "epmsyscpuinfo"), "kernel_cpu_max"))

cursor.execute(req)
for fields in cursor:
  print fields

To iterate over Rows instead, use the row interator:

for row in cursor.iterrow():
  print row

Parameters may also be provided. The meaning of a parameter is backend dependent. They may be used to filter the set of rows returned or to control the formatting of the response.

In the following example, a parameter tells the vlan module that we want to format the response with the “SHOW_VLAN” action:

req = dbapi.request()
req.add_index(dbapi.Column(("vlan", "vlanProc"), "name1"))
req.add_field("name",      dbapi.Column(("vlan", "vlanProc"), "name1"))
req.add_field("tag",       dbapi.Column(("vlan", "vlanProc"), "tag"))
req.add_field("ipAddress", dbapi.Column(("vlan", "vlanProc"), "ipAddress"))
req.add_field("vr",        dbapi.Column(("vlan", "vlanProc"), "name2"))
req.add_param(dbapi.Column(("vlan", "vlanProc"), "action"), "SHOW_VLAN")

Finally, a Request can join multiple tables. In the following, two tables are joined. The first add_param() defines a required filter. The second add_param() defines the relationship between the two joined tables:

req = dbapi.request()
req.add_index(dbapi.Column(("vlan", "show_ports_info_detail"), "port"))
req.add_field("port",          dbapi.Column(("vlan", "show_ports_info_detail"), "port"))
req.add_field("displayString", dbapi.Column(("vlan", "show_ports_info_detail"), "displayString"))
req.add_field("admin",         dbapi.Column(("vlan", "show_ports_info_detail"), "adminState"))
req.add_field("link",          dbapi.Column(("vlan", "show_ports_info_detail"), "linkState"))
req.add_field("rx",            dbapi.Column(("vlan", "show_ports_utilization"), "rxBwPercent"))
req.add_field("tx",            dbapi.Column(("vlan", "show_ports_utilization"), "txBwPercent"))

req.add_param(dbapi.Column(("vlan", "show_ports_info_detail"), "portList"), "*")
req.add_param(dbapi.Column(("vlan", "show_ports_utilization"), "portList"),
                   dbapi.Column(("vlan", "show_ports_info_detail"), "port"))

The frontend’s join implementation is simplistic, but convenient. There will always be a “main” table that drives the join. By default, this is the first table added to the Request. The frontend is limited to looping over just the main table. Other tables are joined in using the constraints set by the parameters.

CM Backend Overview

The cmbackend module allows a Python process to participate in the EXOS data access layer and integrate with EXOS frontends, such as the CLI. Below is an overview of the module’s functionality.

As the cmbackend receives requests from frontends, routes them to the process’s agent. To be a CM Backend, a Python process must implement an agent and pass an instance cmbackend_init():

class MyCmAgent(api.CmAgent):
  pass

agent=MyCmAgent()
api.cmbackend_init(agent)

The agent will be called for events and actions.

An event notifies the agent of state transitions within CM itself. See CmAgent for a list of events. Every agent must implement at least the load_complete and generate_default events to and call ready():

class MyCmAgent(api.CmAgent):
    def event_load_complete(self):
        # CM is done calling actions to load our config.
        # We can prepare ourselves by starting any threads, services, etc.
        # Finally, we need to tell EXOS, we are ready.
        api.ready()

    def event_generate_default(self):
        # CM has no config for us, so we should use our default config.
        # We can prepare ourselves by starting any threads, services, etc.
        # Finally, we need to tell EXOS, we are ready.
        api.ready()

An action is called when a request is sent to the process. The cmbackend module deserializes the request, builds a context, and calls the action named table_name_method:

class MyCmAgent(api.CmAgent):
    def mycfg_get(self, context):
        # This action is called for a "get" request on the "mycfg" table.
        pass

    def mycfg_set(self, context):
        # This action is called for a "set" request on the "mycfg" table.
        pass

The request parameters are available as a dictionary within the context:

class MyCmAgent(api.CmAgent):
    def mycfg_set(self, context):
        param1=context.params["param1"]
        param2=context.params["param2"]

Response fields are also returned via the context:

class MyCmAgent(api.CmAgent):
    def mycfg_get(self, context):
        context.fields["resp1"]=42
        context.fields["resp2"]="LtUE"

The context can also be used to handle indexed tables:

class MyCmAgent(api.CmAgent):
   def mylist_get(self, context):
       idx=context.params.get(idx)

       # Determine the desired index for this list.
       if context.is_getnext:
           if not idx:
               # Start with the first row
               idx=0
           else:
               # Get the next row
               idx+=1
       else:
           # Get a given row
           if not idx:
               context.raise_error("Missing idx")

       # Return the data
       try:
           data=get_mylist_data(idx)
           if data:
               context.fields["resp1"]=data["resp1"]
               context.fields["resp2"]=data["resp2"]
               if get_mylist_data(idx+1):
                   context.more()
           else:
               context.raise_error("Unknown idx")

Tables can be persisted to the switch configuration automatically by adding them to the persistent_list when calling cmbackend_init():

api.cmbackend_init(agent, ["mycfg", "mylist"])