C++ VCI API

Introduction

This document covers the specifics of writing a VCI component in the C++ programming language. For general VCI concepts please refer to the VCI API overview documentation.

When creating a brand new implementation one gets to choose a language to build it in based on the needs of the feature and what you are comfortable with. VCI supports Go, C++ and Python 3 for implementing feature components. Like anything there are tradeoffs with each of language for the purposes of implementing a component below are the tradeoffs to consider when choosing C++.

Pros

  • Fast compiled code

  • Fast interface to VCI bus

  • Decent concurrency support

  • Good for string handling

Cons

  • No standard library support for RFC7951 data (JSON libraries are available to bridge the gap, such as this one.)

  • No standard library support for generating config files (libraries like '{{mustache}}' exist)

  • No existing library to interface with the dataplane or vplaned

Debian Packaging

DANOS uses the Debian packaging system for packages and we have created helpers to aid in creating VCI components. To use these, we have to setup the debian build environment to use the right helpers and create the component descriptor file (.component file) in the directory.

VCI Helpers

To enable the VCI helper one needs to pass "–with=vci" to the "dh" command in debian/rules. An example is included below.

1 2 3 4 5 6 7 8 9 10 11 #!/usr/bin/make -f # # This debian/rules uses debhelper. # export DEB_BUILD_HARDENING=1 # Uncomment this for verbose logging #export DH_VERBOSE=1 %: dh $@ --with=vci,yang

Component Descriptor

dh-vci uses a component descriptor file in the debian directory to generate system specific configurations. One must also supply this component descriptor file for dh-vci to work properly. A sample is included below.

1 2 3 4 5 6 7 8 9 [Vyatta Component] Name=com.example.myfeature Description=VCI component for My Feature ExecName=/lib/myfeature/vci-myfeature ConfigFile=/etc/myfeature.conf [Model com.example.myfeature.v1] Modules=myfeature-v1,myfeature-interfaces-dataplane-v1,my-feature-interfaces-vhost-v1 ModelSets=vyatta-v1

Component Structure

VCI components implement the logic for a given set of YANG modules. A component owns the data for the modules defined in the ".component" file. In the VCI API there are 6 important concepts, the component, the models the component implements, the features that a model supports (Configuration, Operational State and RPCs) and the client. Components are simply containers for models. Models are containers for the Config, State and RPC objects. The Config object implements the logic to validate and apply configuration data for the YANG modules that a component manages. The State object retrieves and formats operational state data for the YANG modules that a component manages. The RPC object implements RPC handlers for any RPC defined in the managed YANG modules.

The C++ VCI API is defined in the "libvci" library. For the Config and State objects, virtual base classes are defined for the required objects. One implements one of these base classes and passes the object to libvci so that the appropriate calls are made when the operation is requested on the bus. In C++ RPCs are implemented as a virtual Method class that may be implemented or any std::function of the form "std::function<std::string(const std::string&)>". In all cases in the C++ VCI API strings are encoded rfc7951 objects. One may use most standard JSON decoders to read this format. All method invocations can happen concurrently so one must be careful to use proper techniques for concurrent access of managed data.

API Documentation

Component

Creating a component is a relatively straightforward process. One needs to create the objects for the desired functionality, attach them to a model and attach the model to a component object. Finally, one starts the component bus process, and waits for it to exit.

Config Object

The Config Object is as follows:

1 2 3 4 5 6 7 class Config { public: virtual void set(const std::string& input) = 0; virtual void check(const std::string& input) = 0; virtual std::string get() { return "{}"; } virtual ~Config() {}; };

One must implement both "set" and "check" methods with ones own class that inherits from this class definition. One may override the "get" function  but this API is no longer used and is only provided for compatibility with existing components.

State Object

The state object is defined as follows.

1 2 3 4 5 class State { public: virtual std::string get() = 0; virtual ~State() {}; };

RPCs

RPCs are implemented by a class implementing the following virtual base class, or by implementing a "std::function<std::string(const std::string&)>". These are associated with the YANG module name and RPC name when creating the Model object.



1 2 3 4 5 class Method { public: virtual EncodedOutput operator()(const EncodedInput& input) = 0; virtual ~Method() {}; };

Model

A Model is created by associating Config, State and RPC objects with the model name as defined in the ".component" file. When one defines a model, one may chain together the method invocations to implement the full thing declaratively as in the example below. Config and state object lifetimes are managed by the VCI library.

1 2 3 4 5 6 7 8 9 10 vci::Model("net.vyatta.eng.vci.template-cpp.v1") .config(new Config()) .state(new State()) .rpc("vci-template-cpp-v1", "rpc1", new RPC1()) //As vci::Method .rpc("vci-template-cpp-v1", "rpc2", RPC1()) //As std::function .rpc("vci-template-cpp-v1", "rpc3", [](const std::string &in)->std::string { // implement the RPC. return in; });

Component

The Component is the mechanism that starts the process that listens on the VCI Bus for messages and dispatches to the appropriate object in the program. One defines a component by creating the component and attaching the model to it. An example is included below.

1 2 3 4 5 6 7 8 9 10 11 auto comp = vci::Component("net.vyatta.eng.vci.template-cpp") .model(vci::Model("net.vyatta.eng.vci.template-cpp.v1") .config(new Config()) .state(new State()) .rpc("vci-template-cpp-v1", "rpc1", new RPC1()) //As vci::Method .rpc("vci-template-cpp-v1", "rpc2", RPC1()) //As std::function .rpc("vci-template-cpp-v1", "rpc3", [](const std::string &in)->std::string { // implement the RPC. return in; }));

Once defined one can start the component process using the "run" method.

1 comp.run();

The "run" method starts the bus process and returns immediately. One may block until component to exit using the "wait" method.

1 comp.wait();

One may tell the component to cleanup gracefully if required using the "stop" method. If the process exits without calling stop the VCI bus will cleanup automatically.

1 comp.stop();

Client

When one creates a component, one may use the same connection to the VCI Bus as a VCI client to talk to other components. One uses the "client" method of the component to access this part of the connection.

1 auto client = comp.client();

The client can subscribe to notifications, request configuration and state data for a given model, and call RPCs via the VCI bus directly. 

One may also create a new client connection to the VCI Bus if one doesn't need a full component.

1 vci::Client client;

Notifications

One of the most useful operations for the client is to subscribe to and emit notifications on the VCI bus.

Emit

Emitting notifications is easy. One simply uses the "client.Emit" method providing the module name the notification is modeled in, the name of the notification and the body as a RFC7951 encoded string.

1 client.emit("vyatta-routing-v1", "instance-added", "{ \"vyatta-routing-v1:name\": \"blue\" }");

The body of the notification will be validated against the data-model before emission on the bus occurs. This means that an invalid notification will be discarded. Any bus client may emit any notification and no access control is provided to ensure notifications come from a given source. This is a tradeoff was chosen to allow flexibility for integrating with existing open-source projects in the most efficient way possible (e.g. a script that is called when a given event occurs).

Subscribe

One may also subscribe to notifications via the VCI client object. Subscriptions have several buffering mechanisms which may be chosen for a given notification based on the desired semantics. By default all notifications are "infinitely buffered". One may choose to coalesce notifications such that only the last received notification is delivered dropping all previous messages, this can be useful when subscribing to notifications that act like state updates. One may choose to drop or block after some limit. One may also remove any previously instantiated limit.

Subscriptions are not active until one calls "run" on the subscription object and one may stop delivery of a subscription via the "cancel" operation. Subscriptions are delivered via registered functions of the type "std::function<void(constEncodedInput&)>" or implementing the vci::Subscriber interface.

1 2 3 4 5 6 7 auto sub = client.subscribe("vyatta-routing-v1", "instance-added", [](const std::string &body) { std::cout << "got notification vyatta-routing-v1:instance-added: " << body << std::endl; }) sub.run(); // ... sub.cancel();

RPC

The client can call RPCs directly on the bus. This is only for use when calling RPCs between components. On should use CallRPC in the configd API for management interface applications.

1 2 auto call = client.call("vyatta-op-v1", "ping", "{\"host\":\"1.1.1.1\"}"); std::cout << "output: " << call.output() << std::endl;

State

The client can request the state of a given model via the bus directly. This is mostly for the interface from configd to the components.

1 2 auto state = client.state_by_model("com.example.myfeature.v1"); std::cout << "state: " << state << std::endl;

Config

The client can request the configuration for a given model via the bus directly. This generally shouldn't be used as one component shouldn't depend on the configuration of another. This is mostly for the interface from configd to the components.

1 2 auto config = client.config_by_model("com.example.myfeature.v1"); std::cout << "config: " << config << std::endl;