.. _cm: .. module:: exos.api Data Access Layer ================================================ In EXOS, the Data Access Layer is implemented by CM. CM has frontends and backends. * A :ref:`frontend ` is a process that consumes data, such as a user interface. * A :ref:`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. .. _cmfrontend: CM Frontend Overview ------------------------------------------------ The :ref:`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. .. seealso:: :ref:`CM Frontend Reference ` 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. .. _cmbackend: CM Backend Overview ------------------------------------------------ The :ref:`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. .. seealso:: :ref:`CM Backend Reference ` 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 :func:`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 :class:`CmAgent` for a list of events. Every agent must implement at least the *load_complete* and *generate_default* events to and call :func:`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 :ref:`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 :func:`cmbackend_init`:: api.cmbackend_init(agent, ["mycfg", "mylist"])