The Device Management Agent
This chapter provides an overview of the role of the Device Management Agent (DMAN). It introduces the DMAN architecture and describes how to make use of it to manage your device. This chapter explains how to develop a device plugin using the DMAN APIs and introduces the LSQL language, which is used for data manipulation.
Device Management Agent (DMAN) Overview
The Device Management Agent (DMAN) acts as the central hub of the Devicescape Wireless Infrastructure Platform by managing the interactions with the subsystems on a device. Figure 1: Device Management Agent Architecture illustrates the architecture of the DMAN. Users interact with a device through interfaces known as consumers. Consumers, in turn, interact with the Device Management Agent which maintains a runtime data model of the device subsystems. This runtime data model contains both configuration and status information. To refresh a subsystem's status information or change its configuration, the DMAN communicates with one or more plugins. A plugin translates between the runtime data model and a subsystem's underling native configuration and status interface.
The Device Management Agent architecture simplifies device management for consumers. Consumers need not be concerned with how a subsystem stores its own data - through configuration files, proc files, and so on, - or how a subsystem makes its status data available. Instead of querying multiple configuration files to manage several different subsystems, a consumer interacts with a single, common interface provided by DMAN - LSQL. Using LSQL, consumers can make SQL-like queries of the underlying device subsystems.
The Device Management Agent allows a developer to easily add a new subsystem to a device. Developers do not have to hardwire the logic for managing a device into a platform; instead, a plugin will encapsulate the logic for communicating with the device's subsystems. Plugins contain two components: a schema and a provider. The schema contains the logic for modeling subsystem data. The provider contains the logic for passing that data between the subsystem and the DMAN.
The following figure shows the Device Management Agent architecture:
Figure 1 Device Management Agent Architecture
DMAN Consumers
As stated previously, a consumer is the means through which users access a device's subsystems. Consumers typically display status information about device subsystems; they may also allow users to update a subsystem's configuration data. By issuing LSQL queries to the DMAN, a consumer interacts with the appropriate subsystem on a device. To make these queries, a consumer must know the subsystem's data model as exposed by the plugin, but it does not need to have deep knowledge about how the plugin and its underlying subsystem operate.
Consumers are most often administrative interfaces (They appear in the top level of Figure 1: Device Management Agent Architecture.) For example, the Web UI might be used to change a radio setting. The DMAN receives this change requests, implements it in the runtime data model, and calls the appropriate plugin to effect the change in the underlying radio subsystem.
The Devicescape Wireless Infrastructure Platform ships with four Device Management Agent consumers:
- The Web UI
- SNMP
- Clustering
- Command Line Interface
DMAN Plugins (Providers)
Typically, each Device Management Agent plugin models at least one subsystem and is made up of two parts: a schema, which models a subsystem and its data, and a provider which processes change and status requests. (The plugins appear at the bottom of Figure 1: Device Management Agent Architecture.) Often, a plugin is simply called a provider because it provides subsystem data to the DMAN and, ultimately, to consumers. Table 1: List of Device Management Agent Plugins shows the list of plugins supplied with the Devicescape Wireless Infrastructure Platform.
|
Note
|
While the typical case is for a plugin to model a single subsystem, a plugin can model no subsystem or multiple subsystems. Similarly, it is possible for a plugin to have only a schema or only a provider or neither. These atypical cases are outside the scope of this document.
|
A plugin interacts with the Device Management Agent using the DMAN's API. Although it is written in C, the Device Management Agent's API is object oriented, using structures to represent objects. All DMAN API functions start with dman_<object> and operate on structure dman_<object> pointers.
A plugin uses the DMAN API both to build its data model and to interact with the agent. A provider starts and initializes a subsystem and registers the subsystem with the Device Management Agent. The provider is also responsible for validating configuration data - such as checking to see that a URL or an IP address is properly formed.
The following table lists all of the plugins that ship with the Devicescape Wireless Infrastructure Platform.
Table 1 List of Device Management Agent Plugins
|
Plugin
|
Description
|
|
ap.dman
|
Access point (AP) (contains order dependencies between providers)
|
|
channel_planner.dman
|
Channel planner plugin.
|
|
config.dman
|
Configuration
|
|
debug.dman
|
Debug
|
|
dot11.dman
|
IEEE 802.11 subsystem
|
|
interface.dman
|
Network interface
|
|
interface_bridge.dman
|
Bridge interface
|
|
interface_vlan.dman
|
Virtual LAN interface
|
|
ip.dman
|
Internet protocol (IP) service
|
|
log.dman
|
Logging
|
|
ntp.dman
|
Network time protocol (NTP) client service
|
|
radius_user.dman
|
RADIUS user
|
|
serial.dman
|
Serial port service (CLI)
|
|
snmp.dman
|
Simple Network Management Protocol (SNMP) agent
|
|
ssh.dman
|
Secure shell service (CLI)
|
|
system.dman
|
General system parameters for the device
|
|
telnet.dman
|
Telnet CLI service
|
Data Model Overview
This section discusses the Device Management Agent's object-oriented data model. The following topics appear in this section:
The Device Management Agent provides a flexible, "class-based" data model. Consumers interact with this model using the LSQL language -- a "light," SQL-based language that uses a familiar syntax. Plugin providers interact with this model using C APIs.
|
Note
|
The Device Management Agent is implemented in C, so terms such as class, instance, and so on, refer to the data model itself.
|
Classes, Instances, Properties, and Values
A class is a type of container for configuration and status data. Classes can have any number of properties. (The number of properties, however, must be set when the class is first compiled; additional properties cannot be added in the running system.) The properties provide a name for configuration and status data that the class is designed to model, and this name is used to access and manipulate this data.
Typically, you create an class object by calling a constructor function that returns a pointer to a structure representing the object. You then manipulate the object through other API functions, supplying a pointer to the structure as one of the parameters. When you create a class, for example, you use the following process:
- You create the class; you are given a pointer to this class.
- You create a property that you want to add to the class; you are given a pointer to this property.
- You call a function, passing in the pointer to the class and to the property, to add the property to the class.
A class serves as the model (template) of the data. The class itself does not contain any values for the properties; it is an abstract model of configuration and status data that is used to create instances of the class. Typical properties hold IP address, access lists, and other configuration and status data that you would expect to find in a wireless access point.
Instances of a class are created to contain the actual data that the class models. When you create a class instance you can assign each property a value, which is the actual configuration data that you want to maintain.
For example, a dns-server class might specify a property of dns-server-address, and no other properties. When a user supplies the actual value for a DNS server -- the server address -- an instance of the class is created. The instances stores the actual value just provided for the dns-server-address property. If the user specifies a second DNS server, a second instance of the dns-server class is created, with the second DNS server address as the value of the dns-server-address property.
The following figure illustrates the relationship of classes, instances, properties, and values.
Figure 2 Device Management Agent Data Model
Types of Classes
Determining the type of class you want to use is one of the most important factors in deciding how to model a subsystem's data. The Device Management Agent has four different kinds of classes, which are discussed in more detail in the following sections:
- Anonymous Classes. Anonymous classes can have multiple instances, but the instances are not named.
- Unique Named Classes. Unique named classes can have multiple instance, but each instance must have a unique name.
- Group named classes. Group named classes can have multiple instances. All instances must be named, but the names do not have to be unique. Instances with the same name form a group.
Instance and Group Names
An instance name is an identifier that allows you to directly access a particular instance (or, in the case of group-named classes, a group of instances.) It is useful for instances to have names when the data you are modeling is logically associated with a meaningful identifier, and you are not just interested in the aggregate of configuration data.
In some cases, however, there is no meaningful identifier you could use to apply to instances of class. For example, log entries have a number (log entry 1, log entry 2, and so on), but these numbers do not have much significance. (It would make little sense, in this context, to search for "log entry 4" as the log entry number has no bearing on the content of the log.) In such cases, in fact, you are interested in the aggregate of the data. You only want to see the log file, which is the concatenation of the text from all the instances of the log class. Anonymous classes are designed to support modeling this kind of data.
The following example, taken from the mac-acl class, illustrates the purpose of using named instances. By listing each mac addresses, users can define privileges for a list of devices, identified by their unique hardware ID (the MAC address). Each instance of the mac-acl class contains a single MAC address. Because the mac-acl class models a list, the class is group-named. You have a meaningful identifier to assign to the instances -- that is, the name of the list -- and by naming the instances you can associate them so that they form groups. Every instance with the same name is part of that group.
Figure 3 Named vs. Unnamed Instances
Arbitrary Data and Device Management Agent Classes
All classes have static definitions; you must take this into account when modeling your data. For example, you might want to create a class for DNS server configuration data (that is, the IP address of the DSN servers you use.) The class could be defined so that each instance had several properties, one for the primary DNS server and additional properties for back up DNS servers. Each property holding a DNS server must have a different name.
If you model the data this way, however, you must decide exactly how many DNS servers you want to allow. Each DNS server would be a property of the class (for example, DNS1, DNS2, DNS3). If you defined a class for four DNS servers, it would not be possible to add a fifth to the class. A class, and by extension, all instances of a class, have a defined set of properties; they cannot acquire more properties once they have been defined.
If you wanted to allow an arbitrary number of DNS servers, you would need to use a different data model. Instead of creating a singleton class with multiple properties to hold the addresses of multiple servers, you would create a class that holds a single server, and use a class type that allows multiple instances. Each time a new server is added, a new instance is created. Thus, the user can specify any number of DNS servers.
Singleton Classes
A singleton class can have only one instance, and that instance cannot be named. For example, if you were creating a class for an NTP client, because a device only needs one NTP client.
Use singleton classes in the following circumstances:
- You are modeling something that is essentially singular in nature.
- You do not need to allow an arbitrary number of properties.
If you were creating a class for an NTP client, you might want to allow the user to specify a number of fallback servers. Typically, you only allow the user to specify a small number of backup servers -- say or four. In such a case, a singleton class is still the best option. You would create 4 properties (such as fallback 2, fallback3, and so on) to add to your NTP singleton class
But if, for any reason, you wanted to allow the user to specify an arbitrary number of fallback servers, you could no longer use a singleton class.
Anonymous Classes
Anonymous classes allow you to create multiple instances of a class without having to supply an identifier for the instances. You usually use anonymous classes when the configuration data needs to be accessed as a single set, and not by individual instances or by groups of instances. Typically, you access this data using the Device Management Agent APIs iterator to loop over and extract data from the unnamed instances of an anonymous class.
Unique Named Classes
Some configuration data needs to be associated with a named instance, thereby allowing the system to directly address an instance by that identifier. For example, you might need to create classes to model access points on a device that supports multiple access points. Each of these access points must have a unique name (for example, "Guest AP") to differentiate them. Using a unique named class is one way of enforcing the requirement that each AP has a unique name, and it also is the most logical way to identify and manipulate the configuration data. (That is, you can easily access the configuration data for an AP by using its unique name.)
Group named classes
Instances of group classes must have names, but these names do not have to be unique. By using the same name for multiple instances, you create a group of instances; these groups can be accessed and manipulated as a set.
From a user's perspective, this type of class allows the creation of certain types of lists. For example, a group named class could be used to create a list of mac addresses that are allowed to access the office LAN, and another list that are only allowed to access the guest LAN.
Classes and Subsystems
In most of the examples discussed in this chapter, there is a 1-1 relationship between classes and subsystems. But a 1-to-many, or a many-to-many relationship is also possible, and is common in more complicated subsystems. The Wireless Subsystem, for example, might use classes for WLAN, VLANs, and SSIDs, and, in turn, the same classes may be used by other subsystems.
Superclasses and Subclasses
For some types of plugins, you may want to use a superclass with subclasses that inherit some but not all of their properties from the superclass. This is an advanced approach that might be called for in some scenarios. However, we suggest that you restrict use of superclasses and subclasses to only those cases which require them. Generally, you should be able to construct most, if not all, of your plugins as simple, flat classes.
In the Devicescape AP, the only example of a superclass is the interface superclass, which has ethernet, loopback, service-set, wds, bridge, and vlan as subclasses. See the CLI Classes and Properties Reference in the Administrator Guide for user documentation on the interface class and its subclasses.
Configuration Files
Recall that one of the primary purposes of the Device Management Agent is to maintain a central store of configuration data that is expressed according to a standard model. One of the responsibilities of a plugin is to translate between the Device Management Agent's model and the subsystem's native model and storage format.
The Device Management Agent stores its own configuration data (that is, the configuration data for all of the subsystems in the data model) in an XML file. This section discusses the configuration files that the Device Management Agent uses.
The Device Management Agent uses the following configuration files:
- config.xml: the "startup" config, read when the device is powered on.
- rescueconfig.xml: a read-only file included when the platform is built, used when config.xml cannot be read; also used during migration.
- defaultconfig.xml: a file created when boxes are provisioned, containing the "factory default" settings for an individual device. If MAC addresses are not obtained from hardware, they are specified in this file. Used when the device is reset to factory defaults, or during migration.
Startup Configuration
When the device is powered on, the Device Management Agent reads config.xml into memory. The device now has a running configuration, that is, the configuration data that was just copied into memory. Once a device has a running configuration, the Device Management Agent works in concert with the device's plugins to manage subsystem configuration changes requested by consumers.
Plugin providers must register a commit function with the Device Management Agent. The commit function is responsible for passing requests for configuration changes to the appropriate subsystem. Whenever a change occurs -- not necessarily just a change that is relevant to a particular subsystem -- the Device Management Agent calls the commit function for every registered plugin. These functions are called in the order specified by dman_providers_order(). Because powering up the device also counts as a change (that is, from "no configuration" to startup), the commit functions are also called every time the device is powered up.
During development and testing, you might want to read or make manual changes config.xml file outside of the API. You can edit config.xml manually (the file is located in the /etc directory.) If you edit the config.xml file manually, you must restart the Device Management Agent in order for the changes to take effect.
The following example shows a section of a config.xml file:
<description>IEEE 802.11g</description>
<channel-policy>static</channel-policy>
<static-channel>6</static-channel>
<radar-detection>on</radar-detection>
<tx-rx-status>up</tx-rx-status>
<beacon-interval>100</beacon-interval>
<fragmentation-threshold>2346</fragmentation-threshold>
<rts-threshold>2347</rts-threshold>
<load-balance-disassociation-utilization>0</load-balance-disassociation-utilization>
<load-balance-disassociation-stations>0</load-balance-disassociation-stations>
<load-balance-no-association-utilization>0</load-balance-no-association-utilization>
<ap-detection>off</ap-detection>
<station-isolation>off</station-isolation>
<rate-limit-enable>off</rate-limit-enable>
<rate-limit>50</rate-limit>
<rate-limit-burst>75</rate-limit-burst>
Default and Rescue Configuration Files
The rescueconfig.xml file is a read-only file that is created when the platform is built.
In some cases, when a device is powered up, the Device Management Agent may not be able to read the config.xml file to get its running configuration. The file might have become corrupt, or, if the device is being powered up for the first time (post-provisioning), the config.xml file might not have been created yet. When this occurs, the Device Management Agent reads configuration data from the defaultconfig.xml file (described below) and then the rescueconfig.xml file.
As the name implies, rescueconfig.xml is used to contain the basic configuration data that subsystems need to get up and running when no other configuration data is available.
The defaultconfig.xml file is typically created when the device is provisioned. It contains the "factory defaults" needed for a particular device. This file often contains critical information such as a device's MAC address.
This file allows the device to be reset to "factory defaults," as may be required, for example, when users lose their passwords. When the device is reset to factory defaults (including a documented default password), the Device Management Agent reads the defaultconfig.xml file and writes this data out to the config.xml file
When a device's firmware is upgraded, the Device Management Agent may use data from the defaultconfig.xml file. A firmware update includes a new rescueconfig.xml file, which contains data needed for the new build of the device. The defaultconfig.xml file, however, might contain critical data that must be maintained, such as the device's MAC address. In such cases, the Device Management Agent reads both the new rescueconfig.xml file and the defaultconfig.xml file to perform a "migration" of the device to its new version.
Developing a Plugin
This section explains how to use the Device Management Agent API to create a typical plugin composed of a schema and a provider. The following topics are discussed:
Overview of Plugin Architecture
Typically, a plugin is composed of two parts: a schema and a provider. The schema models the underlying subsystem (that is, it defines the classes.) The plugin's provider translates between the data model and the subsystem's native configuration and status interface. If, for example, you are writing a plugin for a subsystem that uses an XML configuration file, your provider is responsible for writing configuration data to that file using the format dictated by the subsystem.
|
Note
|
In some cases, a plugin might contain only a schema or only a provider or neither. These cases are outside the scope of this document.
|
A plugin schema models the subsystem objects and their properties. Subsystem properties typically include both subsystem configuration and status information. When a plugin is initialized, the Device Management Agent places a runtime model of the underlying subsystem into the device memory. This runtime data model makes the subsystem's status and configuration available to consumers. For this purpose, every plugin is responsible for registering an initialization function.
As a device is running, consumers query device status information. When a consumer makes a query that requires fresh device status information, the DMAN polls all the plugins for fresh status information. If your plugin contains status information, its provider is responsible for registering a refresh function for this purpose.
Similarly, when a consumer changes a device configuration, the DMAN updates the runtime data model to reflect the change and then calls the plugin to make the same change in the underlying subsystem. If your plugin contains configuration information, its provider is responsible for registering a change function for this purpose.
For the complete API reference, please see Device Management Agent (dman) API Reference.
Plugin Entry Point
All plugins must implement dman_plugin_init(). This is a plugin's main function, and it is typically used to call the other functions needed to execute your plugin, as shown in the following example:
1 void dman_plugin_init(void)
3 dman_plugin_new("1.0");
6 setup_ntpclient_provider();
As shown on line 3, your implementation of dman_plugin_init() must always call one API function, dman_plugin_new(), passing in the version number of the plugin API, which is always 1.0. (The version number is not related to the version of your plugin.) The other functions in the example are not API functions; they are part of the plugin and used to split up the schema and provider parts of the plugin.
Creating the Plugin Schema
Before you begin to write your plugin, you should review the Device Management Agent's data model and decide how you want to model your data. Creating the schema involves the following process:
- Creating at least one class.
- Creating properties.
- Adding the property to the class.
- Specifying (and implementing) a validation function for the property.
- Adding the class to the Device Management Agent runtime data model.
A plugin can create any number of classes. The following code fragment shows the creation of a simple class and the process used to populate the class with properties.
1 struct dman_class *class = dman_class_new("ntp");
2 struct dman_property *property;
3 property = dman_property_new "status");
4 dman_class_add_property(class, property);
As shown in line 1, you call dman_class_new(), passing in the name of class you want to create. Line 2 creates a new status property which is then added to the class in line 4. Calling dman_class_new() only creates the new class. The class is not added to the schema until you call dman_classes_add() as shown on line 5. The class name is case-sensitive and, by convention, is lower case. Your class name must also be unique. Note that if your class name is not unique, dman_classes_add() will fail.
Validating Properties
For every writable property you add to your class, you must also supply a validation function. Data can be passed down to the plugin (via changes made to the values of properties in your classes) through any consumer that you chose to support. And, of course, one of the major advantages of the Device Management Agent architecture is that it is easy for other people to modify existing consumers to send data to your plugin. So, even if you validate your data outside the plugin itself (for example, by using the Web UI), it is still important (and required) that the plugin itself validate the data.
The Device Management Agent provides a large number of utility functions (declared in input.h) that you can use to assist with common types of input validation. The following example shows user-defined function that calls dman_input_is_up_down() to test that the value supplied for a property is of the expected type, that is, a server status of "up" or "down."
1 static int status_is_valid(const char *value)
3 /** status - Controls whether this is on or off.
4 values: "up" or "down". */
5 return dman_input_is_up_down(value, strlen(value));
For your validation function to be called, you must register your validation function with the Device Management Agent. You do this by calling dman_property_set_is_valid_fn(), passing in a pointer to your validation function.
The following example shows the registration of the user-defined server_is_valid() function:
1 struct dman_property *property;
2 property = dman_property_new("server");
3 dman_property_set_is_valid_fn(property, server_is_valid);
4 dman_class_add_property(class, property);
Failing to provide a validation function for every writable property is an error that will cause your plugin to fail or behave unexpectedly.
Creating the Provider
Recall that a provider translates between the data model and the subsystem's native configuration and status interface. When the Device Management Agent requests refresh data or sends a change to the plugin, the provider component is responsible for returning the latest subsystem status or changing the subsystem configuration.
The provider part of your plugin has the following responsibilities:
- It determines how your plugin is notified about status requests or changes to configuration data.
- It handles the details of changing configuration data in the subsystem. (In some cases, for example, you may just pass the data to the subsystem as a message; in other cases, you might need to write the configuration data to a data file.)
- It provides the runtime data model with new status information when requested.
- It restarts the subsystem whenever there is a relevant change. Note that powering up the device is always considered a change (from a "0 state" to an active state). Your plugin, therefore, is responsible for starting up the subsystem every time the device is powered up.
Creating a provider requires calling two API functions, dman_provider_new() and dman_providers_add(), as shown in the following example:
1 static void setup_ntpclient_provider(void)
3 struct dman_provider *provider = dman_provider_new("ntpclient");
4 struct dman_committer *committer = dman_committer_new();
5 dman_committer_set_instance_change_fn(committer, "ntp", change, 0);
6 dman_provider_set_commit_fn(provider,
7 dman_committer_fn, committer);
9 dman_providers_add(provider);
Committing Changes to Configuration Data
If your plugin contains configuration data, its provider must implement a change function. When a consumer changes configuration data, the Device Management Agent applies the change in the runtime data model and then passes the change information (dman_commit_info) to the device's plugins. If the change information contains changes related to a specific subsystem, the responsible provider calls its change function to effect the change. The Device Management Agent provides two approaches for effecting changes to a subsystem's configuration:
- You can use the Device Management Agent committer helper API together with your change function. The committer defines a number of callbacks that can make committing changes easier. These callbacks check the change structure to see if it contains information related to the subsystem. Then, these callbacks pass both the changed subsystem-specific information to your change function and the change structure. So, using this technique, your change function does not have to loop over the change structure searching for relevant changes.
- You can bypass the committer helper and use only your change function. In this case, the Device Management Agent calls your change function whenever any change occurs to any configuration data. Your change function must determine if the changes passed by the DMAN affects its specific subsystem and then commit the changes appropriately.
Regardless of which approach you use, your provider is responsible for registering the its change function with the Device Management Agent.
Choosing an Approach for Committing Changes
The committer helper is designed to simplify the process of committing changes. When you use the committer, you do not have to test that changes have occurred to your configuration data or loop over configuration data. Instead, the committer calls your change function only when a change has occurred to data that your provider is responsible for committing.
The committer defines a number of callbacks that allow you to more easily distribute the work of committing changes. For example, you could implement a committer callback function that is called only when certain properties of a class are changed (dman_committer_property_change_fn_t), and implement another callback that is called on all changed instances, no matter what property has changed (dman_committer_instance_change_fn_t).
When you use the committer, the committer always passes the appropriate instances, properties, and so on; you do not have to extract them. For this reason, using the committer can simplify the process of changing more complicated data models.
If you do not use the committer, your provider's change function is responsible for handling the entire change process. Your change function must extract the instance or instances you need to change from the dman_commit_info structure and effect the changes in the underlying subsystem.
Using the Committer Helper to Commit Changes
Using the committer helper involves the following process:
- Creating a committer helper
- Registering one or more functions that the committer will call
- Registering your provider's committer with the Device Management Agent
- Writing your change function
The following example, taken from the mac-acl plugin, demonstrates this process.
1 void setup_mac_acl_provider(void)
3 struct dman_provider *provider = dman_provider_new("mac-acl");
5 struct dman_committer *committer = dman_committer_new();
6 dman_committer_set_group_change_fn(committer, "mac-acl", change);
7 dman_provider_set_commit_fn(provider,
8 dman_committer_fn, committer);
10 dman_providers_add(provider);
As shown on line 5, a new committer object is created. On line 6, the mac-acl provider registers a committer callback, dman_committer_set_group_change_fn(). This function takes as its arguments, a committer, the class whose group change function you wish to set, and a pointer to your change function - change().
Regardless of whether you use the committer helper or not, a plugin which contains configuration data must always register a change function by calling dman_provider_set_commit_fn(). Because this example uses the committer helper, it sets the commit function to dman_committer_fn(). As shown in lines 7-8 of the example, you pass dman_committer_fn() as the second parameter to dman_provider_set_commit_fn() and the commiter itself as the third argument. As a result, when the Device Management Agent sends change data down to the device's subsystems, the mac-acl provider calls the committer helper. The committer helper determines if the change impacts any instances of the mac-acl class and calls the registered change function (line 6).
Your change function must conform to parameters of the committer helper function it is set to use. In this example, the change() function must conform to a signature of type dman_committer_group_change_fn_t:
void dman_committer_group_change_fn_t(struct dman_instances *instances,
struct dman_commit_info *info)
Continuing with the mac-acl example, this is the function's implementation:
1 static void change(struct dman_instances *instances,
2 const char *class_name,
3 const char *group_name,
4 struct dman_commit_info *info)
6 write_mac_file(instances, class_name, group_name);
7 restart_bsses(instances, group_name, info);
On line 6, the user-defined function write_mac_file() is called; this function initiates the process of writing out the configuration data in the manner required by the subsystem. (Recall that a plugin must translate between the Device Management Agent's data model and the native configuration used by the subsystem. This example shows one such translation.)
In the case of mac-acl, the native format of the configuration data is a list of MAC addresses written out to a file. The group name of the mac-acl is used as the name of the file. So, the MAC address of all instances with the same name are written out as a list in the same file. The implementation write_mac_file() demonstrates this process. It also shows the use of the API's dman_instances_iterator, which loops over all the instances of the group.
1 static void write_mac_file(struct dman_instances *instances,
9 dman_debug_eprintf("Name of mac-acl must not be null.\n");
13 strcpy(buf, ACL_PATH);
18 struct dman_instances_iterator *iterator =
19 dman_instances_new_group_iterator(instances, class, name);
21 while (dman_instances_iterator_has_next(iterator)) {
22 struct dman_instance *mac_acl =
23 dman_instances_iterator_next(iterator);
25 const char *mac = dman_instance_get_value(mac_acl, "mac");
26 fprintf(f, "%s\n", mac);
29 dman_instances_delete_iterator(iterator);
Committing Changes without the Committer
If you decide not to use the committer helper, your plugin's provider is responsible both for supplying a change function and registering the function with the Device Management Agent. Remember, your provider's change function must both determine if a change was made that affects the subsystem and commit those changes.
Your change function must confirm to the expected signature as defined in provider.h:
void dman_provider_commit_fn_t(struct dman_commit_info *info)
The function that implements the callback must be registered with the provider using dman_provider_set_commit_fn(), as shown in the example (taken from the serial plugin):
1 static void setup_getty_provider(void)
3 struct dman_provider *provider = dman_provider_new("getty");
4 dman_provider_set_commit_fn(provider, commit, NULL);
5 dman_providers_add(provider);
Notice that on line 4 there is no committer specified, a NULL value is passed for that argument. On line 4, a pointer to the implementation of the commit callback is passed in as the second argument to the function. The following example shows the commit() function's implementation:
1 static void commit(struct dman_commit_info *info)
3 struct dman_instances *after = dman_commit_info_get_after(info);
4 struct dman_changes *changes = dman_commit_info_get_changes(info);
6 if (dman_changes_contains_class(changes, "serial")) {
7 start(dman_instances_get_singleton(after, "serial"));
Because you are not using the committer, your change function must determine if changes have occurred to a class that contains configuration data. Recall that the provided change data (commit_info) contains information about all changes that have occurred - which may include other subsystem changes irrelevant to your provider. Your change function must locate the relevant changes among all the change information.
In this serial plugin example, the plugin is only responsible for changes to a singleton class, the serial class. On line 6, the commit() calls the dman_changes_contains_class() to determine if the changes that have occurred include any changes to the serial class. If changes have occurred, the plugin calls a user-defined function, start(), which handles the details of committing the changes and restarting the subsystem if necessary.
On line 7, when the provider calls the user-defined start() function, it also extracts the instance that the plugin is responsible for. It does this by calling the dman_instances_get_singleton() function and using the return value, a pointer to the instance of the serial class, as an argument for the function.
The serial plugin's start() function has the following implementation:
1 static const char *SERVICE = "getty";
3 static void start(struct dman_instance *instance)
5 const char *status = dman_instance_get_value(instance, "status");
6 if (status != NULL && strcmp(status, "down") == 0) {
7 dman_service_stop(SERVICE, SIGTERM, 1000000);
11 /* (re)Start getty. */
12 if (! dman_service_is_running(SERVICE)) {
13 dman_service_set_exited_fn(SERVICE, exited, NULL);
14 dman_service_execl(SERVICE,"/sbin/getty","-L",
15 "115200","ttyS0",NULL);
On line 5, the provider gets the value of the status property by calling the dman_instance_get_value() function. This value can be either up or down. If the status is down, the provider calls dman_service_stop().
Starting the Subsystem Executable
A plugin is responsible for starting its subsystem executable when the device powers up (and for restarting the executable when required.) On line 12 in this example, the subsystem is started with a call to dman_service_execl(). The arguments passed to this function include the path to the executable (/sbin/getty) and the service name (getty) is defined on line 1.)
Refreshing Status Data
When a consumer makes a query that requires fresh status data, the data in the Device Management Agent runtime data model is considered stale and is refreshed. When a consumer makes a query that needs fresh data, the DMAN requests a refresh of all subsystems. If your plugin models status data, your provider must implement a refresh function that is responsible for refreshing your particular subsystem data. You can choose one of two approaches for refreshing subsystem data:
- You use the Device Management Agent refresher helper together with your refresh function. refresh.h defines a number of callbacks that can assist your refresh. These callbacks are made only when a relevant request has occurred, so your functions do not have to test to see if the data was requested on your specific subsystem.
- You do not use the refresher helper. Your refresh function is called whenever any request is made for fresh status data. Your function must determine if the refresh involves your plugin's specific subsystem(s) and then refresh the data appropriately.
Regardless of which approach you use, your provider is responsible for registering a refresh function with the Device Management Agent.
Choosing a Refresh Strategy
How you refresh status information depends on how you have implemented your data model. In some cases, if you are using a class that contains only status data, you can simply delete all existing instances of your class and then create new instances with the fresh data.
You can use the refresher helper supplied by the Device Management Agent to assist your refresh. If you use the refresher, you do not have to test to see if your plugin actually needs to perform a refresh, the Device Management Agent handles this for you. This strategy is best for cases where the number of instances is static.
The refresher helper is useful when your model requires refreshing individual instances of a class or if you need to handle different refresh scenarios. If you have no need to refresh individual instances, you can bypass the refresher helper and register a single refresh function. This function is called every time a consumer query requires new status information. Your refresh function must handle all the work of performing the refresh.
Using the Refresher Helper
Similar to the process used with the committer, using the refresher involves the following process:
- Creating a refresher helper
- Registering which refresher functions you wish to use
- Registering the refresher
- Writing a refresh function
For this example, the plugin declares a radio class which has read-only properties, channel and frequency. These properties are not persistent, meaning they are not saved in the startup configuration and only represent the device's status at a particular point in time.
18 void setup_radio_class(void)
22 * Description: Represents a physical radio.
24 struct dman_class *class = dman_class_new("radio");
25 dman_class_set_persistence(class, 1);
26 dman_class_set_type(class, DMAN_CLASS_TYPE_UNIQUE_NAMED);
28 struct dman_property *property;
30 property = dman_property_new("channel");
31 dman_property_set_writable(property, 0);
32 dman_property_set_persistence(property, 0);
33 dman_class_add_property(class, property);
35 property = dman_property_new("frequency");
36 dman_property_set_writable(property, 0);
37 dman_property_set_persistence(property, 0);
38 dman_class_add_property(class, property);
40 dman_classes_add(class);
After establishing the radio class, you write the provider for the radio plugin. Within the plugin, you create the refresher (line 54). dman_refresher_set_instance_fn() takes as arguments the refresher whose function to set and the refresh function name - refresh().
43 void setup_radio_provider(void)
45 struct dman_provider *provider = dman_provider_new("radio");
47 struct dman_committer *committer = dman_committer_new();
48 dman_committer_set_instance_create_fn(committer,"radio", radio_create);
49 dman_committer_set_instance_change_fn(committer,"radio",
51 dman_provider_set_commit_fn(provider, dman_committer_fn, committer);
53 struct dman_refresher *refresher = dman_refresher_new("radio");
54 dman_refresher_set_instance_fn(refresher, refresh);
55 dman_provider_set_refresh_fn(provider, dman_refresher_fn, refresher);
57 dman_providers_add(provider);
Regardless of whether you use the refresher helper or not, if your plugin schema contains status data, you must always register a refresh function by calling dman_provider_set_refresh_fn(). Because this example uses the refresher helper, the function registered dman_refresher_fn() (line 55). The final argument is the refresher object.
As a result of this code, when the Device Management Agent requests a refresh, the radio plugin calls the refresher helper. The refresher will determine if the refresh request pertains to the radio class and, if so, call the registered function, dman_refresher_set_instance_fn(), which calls the refresh() function (line 54) for each instance that needs refreshing.
Your refresh callback must conform to the parameters of the refresher helper function your plugin set. In this example, the refresh() function must conform to a signature of type dman_refresher_instance_fn_t:
void dman_refresher_instance_fn_t (struct dman_instance *instance,
struct dman_refresh_info *info)
The actual activity of refreshing the radio's status information is accomplished with the following:
1 static void refresh(struct dman_instance *instance,
2 struct dman_refresh_info *info)
4 char buf[1024], line[128];
7 const char *name = dman_instance_get_name(instance);
8 snprintf(buf, sizeof(buf), "/proc/net/ieee80211/%s/config",
16 /* Need to read proc file with in one piece,
17 * so use large enough buffer. */
19 setbuffer(f, buf, sizeof(buf));
21 while (fgets(line, sizeof(line), f)) {
23 pos = strchr(line, '=');
29 /* Remove trailing newline, if any. */
30 end = strchr(pos, '\n');
35 if (strcmp(line, "channel") == 0) {
36 dman_instance_set_value(instance,
38 } else if (strcmp(line, "freq") == 0) {
39 dman_instance_set_value(instance,
46 dman_instance_set_value(instance, "mac", get_mac(name));
The above refresh() sets only those properties which are status properties in the radio class. In the event that the class also contained configuration properties, this refresh would ignore the configuration properties.
Refreshing Status Data without the Refresher
If you decide not to use the refresher helper, your provider is responsible both for supplying a refresh function and registering the function with the Device Management Agent. Remember, your provider's refresh function must both determine if a refresh is being requested from your subsystem and refresh the data appropriately.
Your refresh function must conform to the expected signature as defined in provider.h and is as follows:
void dman_provider_refresh_fn_t (struct dman_refresh_info *info)
The function that implements the refresh must be registered with the provider using the dman_provider_set_refresh_fn() and a NULL value is passed in the refresher argument as the refresher is not used. This is shown in the example below:
48 void setup_log_entry_provider(void)
50 struct dman_provider *provider = dman_provider_new("log_entry");
52 struct dman_committer *committer = dman_committer_new();
53 dman_committer_set_instance_create_fn(committer,"log_entry",
55 dman_committer_set_instance_change_fn(committer,"log_entry",
57 dman_provider_set_commit_fn(provider, dman_committer_fn, committer);
58 dman_provider_set_refresh_fn(provider, log_entry_refresh, NULL);
60 dman_providers_add(provider);
In the following example, taken from the log plugin, the plugin uses an anonymous class (a class that can have multiple, unnamed instances) called log-entry. A log-entry represents status data (that is, text messages written to a log) and does not contain any configuration data.
When a consumer, such as the Web UI, makes a request to display the status data - in this case, the collection of log-entry instances that make up the representation of a log file - the Device Management Agent must supply fresh log data. The DMAN calls the log plugin which purges the existing log entries recorded in the model, reads the log file again, and updates the data model with the new data. The Device Management Agent then returns this new data to the consumer. This process is shown here:
1 static void log_entry_refresh(struct dman_refresh_info *info)
3 if (strcmp(dman_refresh_info_get_class(info), "log-entry") != 0) {
7 struct dman_instances *instances =
8 dman_refresh_info_get_instances(info);
10 struct log_state *state = LogState;
11 struct log_list *list = &(state->user_event_list);
13 struct log_entry *entry;
16 dman_instances_delete_class(instances, "log-entry");
18 if (list->head == NULL) {
24 struct dman_class *class = dman_classes_get("log-entry");
25 struct dman_instance *i = dman_instance_new(class, NULL);
27 char num_entry_str[16];
29 snprintf(num_entry_str, 16, "%d", num_entry);
30 dman_instance_set_value(i, "number", num_entry_str);
31 dman_instance_set_value(i, "priority", entry->priostr);
32 dman_instance_set_value(i, "time", entry->timestr);
33 dman_instance_set_value(i, "daemon", entry->daemon);
34 dman_instance_set_value(i, "message", entry->message_body);
35 dman_instances_add(instances, i);
38 } while (entry != list->head);
On line 16, all the existing instances of the anonymous log-entry class in the data model are deleted, by calling the API function dman_instances_delete_class(). In this case, the refresh can delete all the instances of the class because the class has no configuration data - it only models status data.
In the loop on lines 23-38, the instances of the class are recreated. Each iteration of the loop creates a new log-entry instance, and populates the instances with data from the log file.
Building a Plugin
- Load the image on the target device as described in the topics on booting and loading the kernel in the Getting Started Guide for your target board.
Debugging with the Device Management Agent
Because the Device Management Agent must be running to make for a device to be accessible in a production environment, the Device Management Agent must be automatically restarted if it crashes. A kernel watchdog is periodically updated by the Device Management Agent, and if the watchdog is not updated within a given timeout interval, it will restart the device. (Restarting the device does, of course, cause Device Management Agent to restart).
Disabling the Watchdog
During development and debugging, you may not want the device to be automatically restarted. You can disable the watchdog by issuing the following procedure.
To disable the watchdog,
- Access the CLI, and then escape to the shell by using an exclamation point (!).
- Get the process id of the watchdog using the
ps command.
- Issue the following command:
kill -SIGUSR1 [process ID]
Device Management Agent Debug Levels
The Device Management Agent API provides a variety of debugging information to help you during development. You can also make use of the API's functions that output debugging information in your own plugins. Debugging information is generally sent to standard error, but you can direct the output elsewhere.
Debugging messages are prefixed with the string dman to help distinguish them from other messages.
The Device Management Agent uses debug levels to determine how much debugging information should be sent to standard error.
The maximum debug level is the value of the DMAN_DEBUG_MAX constant. In the reference access point, DMAN_DEBUG_MAX is set to six.
At level 1 (the lowest level is 0), the Device Management Agent outputs information about errors that are made in using the API.
The dman_committer_set_property_change_fn() function, for example, requires the user to pass in pointers for the provider's committer, the class containing the property, the property itself, and the properties's change function.
The API checks to make sure that the user has not passed in null pointers. If the debugging level is set to 1 or higher, the debug message ("Null commiter," and so on) is sent to standard error.
Higher debug levels (5 and 6) are used to output information about what the Device Management Agent is doing -- such as committing classes, calling pre-commit functions, and so on.
The following code snippet shows a debug message that lets you know when a class is being committed. (In the example, the debug level must be set to 5 or higher for you to receive notification.)
2 " Committing class %s,"
Adding Debugging Information to Plugins
The Device Management Agent API provides a number of functions to output debugging information. These are the two most commonly used:
void dman_debug_1printf(const char *fmt, ...);
and
void dman_debug_lprintf(int level, const char *fmt, ...);
The dman_debug_1printf() function will print the specified message whenever the debug level is 1 or greater (that is, when debugging is turned on.)
The second function, dman_debug_lprintf(), takes an additional argument, which is the debug level of the message. This function only prints its message if the debug level is equal to or greater than the specified level.
Updating Status and Configuration Files
In some cases, when you add or modify a subsystem, you need to make manual changes to configuration files.
When Adding a Subsystem
- If you want your subsystem to start up with a default configuration, you need to modify the rescueconfig.xml to include the default configuration. The Device Management Agent uses this file when config.xml is not found or cannot be read because of errors. If your subsystem requires some basic default values in order to start, you should include that information in this file.
- If you want to be able to configure or view status information though the CLI, you will need to make changes to the schema that the CLI uses. This process is documented in the next section.
- If the subsystem needs to share configuration and status information through the clustering system, you must make changes to the schema that the clustering system uses. Please see Implementing Clustering of this manual.
CLI Schema Files
How DMAN Plugins are Exposed in the CLI
The command line interface (CLI) allows you to manipulate Device Management Agent (DMAN) configuration data using a simple syntax. The CLI is used frequently during development to create and test instances of configuration classes. It can also be used by other subsystems to record changes in configuration data.
Plugin classes and properties that are exposed to the CLI are defined in a text file per the format shown in schema-format.txt, which is provided in the clicmds package. (To extract the source for the clicmds package, use tsrpm --extract <PackageName>.src.rpm, then navigate into the source directory to find the file schema-format.txt.) The schema maps to the CLI internal structure as follows:
class-name summary type flags named_after
class prop-name summary flags
Most of the classes and properties exposed to the CLI for the Devicescape Reference AP are defined in the file ap.cli. Some are defined in functional-specific CLI files such as ntp.cli, web_server.cli, and so forth.
Use the schema-format.txt file and existing .cli files as guides for creating your own new CLI definitions.
Making Your Custom Classes and Properties Available in the CLI
When you create new classes using the schema API, these are not immediately available through the CLI. If you want to make them available you must either:
- Edit the ap.cli file to add definitions for your new classes and properties
Or
- If you are adding a new custom package, include your own <some-name>.cli file of the same format in your new package. This .cli file should include definitions for those classes and properties provided by the custom package that you want to expose through the CLI.
Any file found in the directory /usr/lib/clicmds/schema/ will be read to construct the CLI schema.
Any number of classes and properties can be defined in a single file. Classes must be defined before any of their properties. Files are read in alphabetical order, which makes it possible to define properties in one file and ensure that the class is defined first.