Go to main content

Remote Administration Daemon Developer's Guide

Exit Print View

Updated: April 2020
 
 

Designing RAD Components

The components that are fundamental to RAD are interfaces, objects that implement those interfaces, and the namespace in which those objects can be found and operated upon.

RAD APIs

The users of the RAD APIs belong to two categories. administrators and developers. Accommodating both categories of consumers within one interface is difficult. Administrators require task-based APIs which match directly onto well-understood and defined administrative activities. Developers require detailed, operation-based interfaces which may be aggregated to better support unusual or niche administrative activities.

For any given subsystem, you can view existing command-line utilities (CLIs) and libraries (APIs) as expressions of the rad APIs. The CLIs represent the task-based administrative interfaces and the APIs represent the operation-based developer interfaces. The goal in using RAD is to provide interfaces that address the lowest-level objectives of the audience. If the audience are administrators (task-based), this effort could translate to matching existing CLIs. If the audience are developers, this effort could mean significantly less aggregation of the lower-level APIs.

APIs are the primary deliverable of a RAD module. The API acts as the name root for all components of the API, defining a namespace which identifies objects to client. APIs are versioned and a single RAD instance is capable of offering multiple major versions of APIs to different clients. RAD modules are a grouping of interfaces, events, methods, and properties which enable a user to interact with a subsystem.

When exposing the elements of a subsystem consider carefully how existing functions can be grouped together to form an interface. Imperative languages, such as C, tend to pass structures as the first argument to functions, which provides a clear indicator as to how best to group functions into APIs.

RAD API Versions

A version element is required for all APIs. See RAD Interface Versioning for more details about API versions.

RAD API Namespace and Restricted Names

An API defines a namespace in which all top-level elements are defined. Names of components must be unique. Names must not begin with "_rad" because this string is reserved for toolchain provided functionality.

Synchronous and Asynchronous Invocation in RAD

All method invocations in RAD are synchronous. Asynchronous behavior can be obtained by requiring events to provide notifications. For more information, see RAD Synchronization Functions in C.

Legacy Constraints for RAD APIs

Some CLIs contain processing capabilities that are not accessible from an existing API. Such constraints must be considered in the RAD API design.

Do not duplicate the functionality in the new RAD interface, which would introduce redundancy and significantly increase maintenance complexity. One particular area where RAD interface developers need to be careful is to avoid duplication around parameter checking and transformation. This duplication is likely to be a sign that existing CLI functionality should be migrated to an API.

RAD modules must be written in C. Some subsystems, for example, those written in other languages, have no mechanism for a C module to access API functionality. In these cases, RAD module creators must access whatever functionality is available in the CLI or make a potentially significant engineering effort to access the existing functionality, for example, rewriting existing code in C, embedding a language interpreter in their C module, and the like.

RAD Client Library Support

RAD modules are designed to have a language agnostic interface. However, you might want to provide additional language support through the delivery of a language-specific extension. You should restrict the use of such extensions. Use them only to help improve the fit of an interface into a language idiom.

RAD API Design Examples

Combining the tools described so far in this document to construct an API with a known design can be a challenge. Several possible solutions for a particular problem are often available. The examples in this section illustrate the best practices.

This is only an example. This means it does not reflect the user management modules that are in Oracle Solaris.

RAD User Management Example

Object or interface granularity is subjective. For example, imagine an interface for managing a user. The user has a few modifiable properties:


Note -  This example does not reflect the user management modules in Oracle Solaris.

    Object or interface granularity is subjective. For example, imagine an interface for managing a user. The user has a few modifiable properties:

  • name property of type string

  • shell property of type string

  • admin property of type boolean

The interface for managing this user might consist solely of a set of attributes corresponding to the above properties. Alternatively, it could consist of a single attribute that is a structure containing fields that correspond to the properties, possibly more efficient if all properties are usually read or written together. The object implementing this might be named as follows:

com.example.users:type=TheOnlyUser

If instead of managing a single user you need to manage multiple users, you have a couple of choices. One option would be to modify the interface to use methods instead of attributes, and to add a "user" argument to the methods, for example:

setUserAttributes(username, attributes) throws UserError
attributes getUserAttributes(username) throws UserError

This option is sufficient for a single user, as well as provides support to other global operations such as adding a user, deleting a user, getting a list of users and so on. This option could use a more appropriate name, for example:

com.example.users:type=UserManagement

However, suppose users have more properties and you want to perform more operations, such as, sending them email, giving them a bonus and so on. As the server functionality grows, the UserManagement's API becomes cluttered and the API will have code for both global and per-user operations. The need to specify a user and the associated errors for each per-user operation would start looking redundant.

username[] listUsers()
addUser(username, attributes)
giveRaise(username, dollars) throws UserError
fire(username) throws UserError
sendEmail(username, message) throws UserError
setUserAttributes(username, attributes) throws UserError
attributes getUserAttributes(username) throws UserError

A cleaner alternative would be to separate the global operations from the user-specific operations and create two interfaces. The UserManagement object would use the global operations interface:

username[] listUsers()
addUser(username, attributes)

A separate object for each user would implement the user-specific interface:

setAttributes(attributes)
attributes getAttributes()
giveRaise(dollars)
fire()
sendEmail(message)

Note -  If fire operates more on the namespace than the user, it should be present in UserManagement where it would need to take a username argument.

Finally, the different objects would be named such that the different objects could be easily differentiated and be directly accessed by the client:

com.example.users:type=UserManagement
com.example.users:type=User,name=ONeill
com.example.users:type=User,name=Sheppard
...

This example also highlights a situation where you do not want the RAD server to enumerate all objects when a client issues a LIST request. Pulling down a list of potentially thousands of objects on every LIST call does not benefit the majority of clients.

RAD Interface

An interface defines how a RAD client can interact with an object. An object implements an interface, providing a concrete behavior to be invoked when a client makes a request.

The primary purpose of RAD is to consistently expose the various pieces of the system for administration. Not all subsystems are alike. However, each has a data and state model tuned to the problems they are solving. Although there are major benefits to using a common model across components when possible, uniformity comes with trade-offs. The increased inefficiency and client complexity, and risk of decreased developer adoption, often warrant using an interface designed for problem at hand.

An interface is a formal definition of how a client may interact with a RAD server object. An interface may be shared amongst several objects, for example, when maintaining a degree of uniformity is possible and useful, or may be implemented by only one. A RAD interface is analogous to an interface or pure abstract class in an object oriented programming language. In the case of rad, an interface consists of a name, the set of features a client may interact with, optionally a set of derived types referenced by the features, and a version.

    The features that are supported include the following:

  • Methods, which are procedure calls made in the context of a specific object

  • Properties, which are functionally equivalent to methods but differ semantically

  • Asynchronous event sources

RAD Interface Names

Each interface has a name. This name is used by the toolchain to construct identifier names when generating code. When naming an API, interface, or object, module developers have broad leeway to choose names that make sense for their modules. However, some conventions can help avoid pitfalls that might arise when retrieving objects from the RAD server.

RAD Object Names

The domain portion of RAD object names follows a reverse-dotted naming convention that prevents collisions in rad's flat object namespace. This convention typically resembles a Java package naming scheme:

com.oracle.solaris.rad.zonemgr
com.oracle.solaris.rad.usermgr
org.opensolaris.os.rad.ips
...

To distinguish a rad API from a native API designed and implemented for a specific language, include a "rad." component in the API name.

With the goal of storing objects with names consumers would expect, APIs, and the domains of the objects defined within them, should share the same name. This practice makes the mapping between the two easily identifiable by both the module consumer and module developer.

With the same goal of simplicity, identifying an interface object is made easier by adhering to a "type=interface" convention within the object name.

Applying both conventions, a typical API will look like the following example.

    <api  xmlns="http://xmlns.oracle.com/radadr"
       name="com.oracle.solaris.rad.zonemgr">
      <version major="1" minor="0"/>
      <interface name="ZoneInfo"> <!-- Information about the current zone  -->
      <property name="name" access="ro" type="integer"/>

          ...

        </interface>
</api>

Within the module, the API appears as follows:

int
_rad_init(void)

    {
    ...
          adr_name _t *zname = adr_name_vcreate(MOD_DOMAIN, 1, "type", "ZoneInfo");
          conerr_t cerr = rad_cont_insert_singleton(&rad_container, zname, &interface_ZoneInfo_svr);
          adr_name_rele(zname);
 

         if (cerr != CE_OK) {
             rad_log(RL_ERROR, "failed to insert module in container");
             return(-1);
         }
        return (0);
}

On the consumer side (Python), the API appears as follows:

import rad.connect as radcon
import rad.bindings.com.oracle.solaris.rad.zonemgr as zonemgr

# Create a connection and retrieve the ZoneInfo object
with radcon.connect_unix() as rc:
    zinfo = rc.get_object(zonemgr.ZoneInfo())
    print zinfo.name
RAD Case Strategies

In an effort to normalize the appearance of like items across development boundaries, and to minimize the awkwardness in generated language-specific interfaces, several case strategies have been informally adopted.

Module

The base of the API or domain name. For a module that describes an interface domain.prefix.base.adr, module spec files should be named base.adr, and the resulting shared library mod_base.so.

Examples:

/usr/lib/rad/interfaces/zonemgr/version/1/zonemgr.adr
/usr/lib/rad/module/mod_zonemgr.so
API

Reverse-dotted domain, all lowercase.

Examples:

com.oracle.solaris.rad.usermgr
com.oracle.solaris.rad.zonemgr
Interface, struct, union, enum

Non-qualified, camel case, starting with uppercase.

Examples:

Time
NameService
LDAPConfig
ErrorCode
Enum value and fallback

Non-qualified, uppercase, underscores.

Examples:

CHAR
INVALID_TOKEN
REQUIRE_ALL
Interface property and method, struct field, event

Non-qualified, camel case, starting with lowercase.

Examples:

count
addHostName
deleteUser

RAD Feature Types

The common similarity between the three RAD feature types – methods, attributes, and events – is that they are named. The names of all three feature types exist in the same interface namespace and must therefore be unique. For example, you cannot have both a method and an attribute that is called foo. This exclusion avoids the majority of conflicts that could arise when trying to naturally map these interface features to a client environment. As in the API namespace, features must not begin with "_rad" because this string is reserved for use by the RAD toolchain.


Note -  Enforcing a common namespace for interface features is not always enough. Some language environments place additional constraints on naming. For instance, a Java client will see an interface with synthetic methods of the form getfunction_name, setfunction_name, or isfunction_name for accessing attribute function_name that must coexist with other method names. Explicitly defining methods with those names might cause a conflict.
RAD Methods

A method is a procedure call made in the context of the object it is called on. In addition to a name, a method may define a return type, can define zero or more arguments, and may declare that it returns an error, optionally with an error return type.

If a method does not define a return type, it returns no value. It is effectively of type void. If a method defines a return type and that type is permitted to be nullable, the return value may be defined to be nullable.

Each method argument has a name and a type. If any argument's type is permitted to be nullable, that argument might be defined to be nullable.

If a method does not declare that it returns an error, it theoretically cannot fail. However, because the connection to RAD could be broken either due to a network problem or a catastrophic failure in RAD itself, all method calls can fail with an I/O error. If a method declares that it returns an error but does not specify a type, the method may fail due to API-specific reasons. Clients will be able to distinguish this failure type from I/O failures.

Finally, if a method also defines an error return type, data of that type may be provided to the client in the case where the API-specific failure occurs. Error payloads are implicitly optional, and must therefore be of a type that is permitted to be nullable.


Note -  Method names cannot be overloaded.

    The following are the guidelines for methods:

  • Methods provide mechanisms for examining and modifying administrative state.

  • Consider grouping together existing native APIs into aggregated RAD functions which enable higher order operations to be exposed.

  • Follow established good practice for RPC style development. RAD is primarily for remote administration, and avoiding excessive network load is good practice.

RAD Property Attributes

    A RAD attribute is metaphorically a property of the object. Attributes have the following characteristics:

  • A name

  • A type

  • A definition as read-only, read-write, or write-only

  • A method may declare that accessing the attribute returns an error, optionally with an a error return type

Reading a read-only or read-write attribute returns the value of that attribute. Writing a write-only or read-write attribute sets the value of that attribute. Reading a write-only attribute or writing a read-only attribute is invalid. Clients may treat attempts to write to a read-only attribute as a write to an attribute that does not exist. Likewise, attempts to read from a write-only attribute may be treated as an attempt to read from an attribute that does not exist.

If an attribute's type is permitted to be nullable, its value may be defined to be nullable.

An attribute may optionally declare that it returns an error, with the same semantics as declaring (or not declaring) an error for a method. Unlike a method, an attribute may have different error declarations for reading the attribute and writing the attribute.

Attribute names may not be overloaded. Defining a read-only attribute and a write-only attribute with the same name is not valid.

Given methods, attributes are arguably a superfluous interface feature. Writing an attribute of type X can be implemented with a method that takes one argument of type X and returns nothing, and reading an attribute of type X can be implemented with a method that takes no arguments and returns a value of type X. Attributes are included because they have slightly different semantics.

    In particular, an explicit attribute mechanism has the following characteristics:

  • Enforces symmetric access for reading and writing read-write attributes.

  • Can be easily and automatically translated to a form natural to the client language-environment.

  • Communicates more about the nature of the interaction. Reading an attribute ideally should not affect system state. The value written to a read-write attribute should be the value returned on subsequent reads unless an intervening change to the system effectively writes a new value.

RAD Events

A RAD event is an asynchronous notification generated by RAD and consumed by clients. A client might subscribe to events by name to register interest in them. The subscription is performed on an object which implements an interface. In addition to a name, each event has a type.

    Events have the following characteristics:

  • Sequential

  • Volatile

  • Guaranteed

A client can rely on sequential delivery of events from a server as long as the connection to the server is maintained. If the connection fails, then events will be lost. On reconnection, a client must resubscribe to resume the flow of events.

Once a client has subscribed to an event, event notifications will be received until the client unsubscribes from the event. On receipt of a subscribed event, a client receives a payload of the defined type.

    The following are the guidelines for events:

  • The module is responsible for providing a sequence number. Monotonically increasing sequence numbers are recommended for use, since these will be of most potential use to any clients.

  • Consider providing mechanisms for allowing a client to throttle event generation.

  • Carefully design event payloads to minimize network load.

  • Do not try to replicate the functionality of network monitoring protocols such as SNMP.

RAD Commitment Levels

To solve the problem of different features being intended for different consumers, RAD defines two commitment levels: private and committed. All API components: derived types, interfaces and the various interface sub-components (method, attribute, and event) define their commitment levels independently.

Commitment levels provide hints to API consumers about the anticipated use and expected stability of a feature. A feature with a commitment of committed can be used reliably. The private features, are likely to be subject to change and represent implementation details not intended for public consumption.

RAD Interface Versioning

    RAD interfaces are versioned for the following reasons:

  • APIs change over time.

  • A change to an API might be incompatible with existing consumers.

  • A change might be compatible with existing consumers but new consumers might not be able to use the API that was in place before the change occurred.

  • Some features represent committed interfaces whose compatibility is paramount, but others are private interfaces that are changed only in lockstep with the software that uses them.

RAD Version Numbering

RAD uses a major.minor versioning scheme. When a compatible change to an interface is made, its minor version number is incremented. When an incompatible change is made, its major version number is incremented and its minor version number is reset to 0.

In other words, an implementation of an interface that claims to be version X.Y (where X is the major version and Y is the minor version) must support any client expecting version X.Z, where Z <= Y.

    The following interface changes are considered compatible:

  • Adding a new event

  • Adding a new method

  • Adding a new attribute

  • Expanding the access supported by an attribute, for example, from read-only to read-write

  • A change from nullable to non-nullable for a method return value or readable property, that is, decreasing the range of a feature

  • A change from non-nullable to nullable for a method argument or writable property, that is, increasing the domain of a feature

    The following interface changes are considered incompatible:

  • Removing an event

  • Removing a method

  • Removing an attribute

  • Changing the type of an attribute, method, or event

  • Changing a type definition referenced by an attribute, method, or event

  • Decreasing the access supported by an attribute, for example, from read-write to read-only

  • Adding or removing method arguments

  • A change from non-nullable to nullable for a method return value or readable property, that is, increasing the range of a feature

  • A change from nullable to non-nullable for a method argument or writable property, that is, decreasing the domain of a feature


Note -  An interface is more than just a set of methods, attributes, and events. Associated with those features are well-defined behaviors. If those behaviors change, even if the structure of the interface remains the same, a change to the version number might be required.

A RAD client can access version information from a client binding. The mechanism for accessing the information depends on the client language like C, Java, and Python. For example, in Python, the rad.client module contains the rad_get_version() function, which may be used to get the version of an API.