Jump to: navigation, search

OpenDaylight Controller:MD-SAL:Toaster Step-By-Step

Contents

Overview

The following tutorial will breakdown the prebuilt Toaster sample and take you through the Toaster sample step by step as if re-creating it from scratch. We will start with simple definitions that enable access to the operational data only, and advance to the full-blown example that demonstrates many aspects of MD-SAL, including remote procedure calls via JMX and restconf, accessing state data via JMX, notifications and a consumer service. You may follow the steps by actually creating new projects and performing them or by simply reading through and also studying the prebuilt code. Either way, this exercise is useful in understanding in general what you need to do to build MD-SAL plugins from scratch.

The code for the prebuilt Toaster is in the controller project under opendaylight/md-sal/samples and also can be found at Toaster on Github.

If you are looking for an overview of the prebuilt Toaster sample, along with discussions of how the classes interact with each other, check out the Toaster Tutorial'

NOTE: The follow documentation is a work in progress and may have embedded questions and answers. Look for QUESTION to find existing questions.

Toaster Parts

There are several parts that make up this toaster step-by-step example. During this example we illustrate how the yang model is providing abstraction for us, and how MD-SAL provides the plumbing (wiring) to hook everything up.

  • Prerequisite will define the project structure and the parent pom.xml gathering the different modules we'll build.
  • Part 1 of this example will define the toaster data model (north-bound interface) and will provide a read-only implementation to retrieve operational data on the toaster.
  • Part 2 will add and implement a remote procedure call which will allow the user to interact with the operations restconf interface, as well as see status changes to operational data.
  • Part 3 illustrates how a user can modify configuration data via restconf and how our toaster can listener for those changes.
  • Part 4 of this example will provide additional statistical attributes not present in the north bound interface, but available by the implementation via JMX.
  • Part 5 will introduce a KitchenService that is a consumer of the toaster model. This provides a demonstration of how other business intelligence in the controller can access the data models and invoke RPC calls for the purpose of providing additional business logic in the controller.
  • Part 6 will expand our example by adding notifications subscriptions from the toaster provider and consumer that are routed through the MD-SAL.

Prerequisites

  • Java JDK 1.8+
  • Maven 3.3.9+

Part 1: Defining an Operational Toaster

Part 1 of this tutorial will walk you through defining a data model which maps to a toaster, and a service that provides the operational data about the given toaster. The toaster model will be made up of a yang file, some new and modified java classes, and a number of auto-generated java files. The yang file provides another level of abstraction between the north-bound client and the south-bound implementation. It is important to note that the MD-SAL provides the plumbing while the yang data models provide the abstractions.

  1. toaster.yang - This file defines the north bound data model. Specifically, it defines the abstraction of a toaster that is visible to north-bound clients (e.g. the restconf API).

Define the Toaster yang data model

The yang file, toaster.yang, defines the north bound abstraction of the toaster data model, specifically its attributes, RPCs and notifications, that can be accessed by north-bound clients (e.g. the restconf API). This file is located in the toaster project under the api/src/main/yang source folder.

  //This file contains a YANG data definition. This data model defines
  //a toaster, which is based on the SNMP MIB Toaster example 
  module toaster {

    //The yang version - today only 1 version exists. If omitted defaults to 1.
    yang-version 1; 

    //a unique namespace for this toaster module, to uniquely identify it from other modules that may have the same name.
    namespace
      "http://netconfcentral.org/ns/toaster"; 

    //a shorter prefix that represents the namespace for references used below
    prefix toast;

    //Defines the organization which defined / owns this .yang file.
    organization "Netconf Central";

    //defines the primary contact of this yang file.
    contact
      "Andy Bierman <andy@netconfcentral.org>";

    //provides a description of this .yang file.
    description
      "YANG version of the TOASTER-MIB.";

    //defines the dates of revisions for this yang file
    revision "2009-11-20" {
      description
        "Toaster module in progress.";
    }

    //declares a base identity, in this case a base type for different types of toast.
    identity toast-type {
      description
        "Base for all bread types supported by the toaster. New bread types not listed here nay be added in the future.";
    }

    //the below identity section is used to define globally unique identities
    //Note - removed a number of different types of bread to shorten the text length.
    identity white-bread {
      base toast:toast-type;       //logically extending the declared toast-type above.
      description "White bread.";  //free text description of this type.
    }

    identity wheat-bread {
      base toast-type;
      description "Wheat bread.";
    }

    //defines a new "Type" string type which limits the length
    typedef DisplayString {
      type string {
        length "0 .. 255";
      }
      description
        "YANG version of the SMIv2 DisplayString TEXTUAL-CONVENTION.";
      reference
        "RFC 2579, section 2.";

    }

    // This definition is the top-level configuration "item" that defines a toaster. The "presence" flag connotes there
    // can only be one instance of a toaster which, if present, indicates the service is available.
    container toaster {
      presence
        "Indicates the toaster service is available";
      description
        "Top-level container for all toaster database objects.";

      //Note in these three attributes that config = false. This indicates that they are operational attributes.
      leaf toasterManufacturer {
        type DisplayString;
        config false;
        mandatory true;
        description
          "The name of the toaster's manufacturer. For instance, Microsoft Toaster.";
      }

      leaf toasterModelNumber {
        type DisplayString;
        config false;
        mandatory true;
        description
          "The name of the toaster's model. For instance, Radiant Automatic.";
      }

      leaf toasterStatus {
        type enumeration {
          enum "up" {
            value 1;
            description
              "The toaster knob position is up. No toast is being made now.";
          }
          enum "down" {
            value 2;
            description
              "The toaster knob position is down. Toast is being made now.";
          }
        }
        config false;
        mandatory true;
        description
          "This variable indicates the current state of  the toaster.";
      }
    }  // container toaster
  }  // module toaster

You can see above that we marked all three of the leaf attributes on the toaster container as operational (config false), instead of configuration data. MD-SAL, along with some ietf drafts for restconf split the configuration and operational data into two separate data stores.

  • Operational - Operational data stores are used to show the running state (read only) view of the devices, network, services, etc that you might be looking at. In our case we have a service called toaster which is available - the manufacture, model and status of the toaster are all provided by the underlying toaster and can not be configured (later we will add a configuration attribute). Think of the first two attributes as constants which are hardcoded into the physical device, while the third is a representation of current state, and changes as the toaster is used.
  • Config - Config data stores are generally used to configure the device in someway. These configurations are user provided and is a way for the user to tell the device how to behave. For example if you wanted to configure the resource in some way, such as applying a policy or other configuration then you would use this data store. We will add some configuration data in part 3 of this tutorial.

Generate the Toaster yang data model source

At this point we can compile the yang data model to generate the java source files and build a bundle jar file. To do this, we simply need to specify the binding-parent artifact as the parent in the pom.xml:

 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>org.opendaylight.mdsal</groupId>
    <artifactId>binding-parent</artifactId>
    <version>0.11.0-SNAPSHOT</version>
    <relativePath/>
  </parent>

  <groupId>org.opendaylight.controller.samples</groupId>
  <version>1.6.0-SNAPSHOT</version>
  <artifactId>sample-toaster</artifactId>
  <packaging>bundle</packaging>

</project>

The parent pom includes the configuration for the yang-maven-plugin which generates the java source from yang definition files.

Next, run:

mvn clean install

Note: You really only need to run 'mvn install' here since we have nothing to clean, but running a clean will not harm anything and is a good practice to ensure your generated code is cleaned up correctly so new code can be generated.

Now you will see java class files generated under target/generated-sources/mdsal-binding. Classes of note:

  • Toaster - an interface that represents the toaster container with methods to obtain the leaf node data.
  • ToasterData - an interface that represents the top-level toaster module with one method getToaster() that returns the singleton toaster instance.
  • WheatBread, WhiteBread, etc' - abstract classes that represent the various toast types.
  • $YangModelBindingProvider, $YangModuleInfoImpl - these are used internally by MD-SAL to wire the toaster module for use. More on this later.

Implement the operational Toaster provider (OpendaylightToaster)

We've defined the data model for the toaster, now we need an implementation to provide the operational data. We'll create a class OpendaylightToaster. On initialization, it writes the operational toaster data to the MD-SAL's data store, via the DataBroker interface, and deletes the data on close. The final version of this class can be found in the package org.opendaylight.controller.sample.toaster.provider under the src/main/java source folder of the toaster-provider sub-project. The reason this class is created in a separate project is it allows the data model and implementation to be provided by different bundles, thus allowing different bundles to define different implementations of the same data model. There is however nothing stopping us from putting everything into the same bundle if the implementation is proprietary.

A portion of the class is shown below:

public class OpendaylightToaster implements AutoCloseable {
  
   private static final InstanceIdentifier<Toaster> TOASTER_IID = InstanceIdentifier.builder(Toaster.class).build();
   private static final DisplayString TOASTER_MANUFACTURER = new DisplayString("Opendaylight");
   private static final DisplayString TOASTER_MODEL_NUMBER = new DisplayString("Model 1 - Binding Aware");
    
   private DataBroker dataBroker;
  
   public OpendaylightToaster() {
   }
   
   public void setDataBroker(final DataBroker dataBroker) {
       this.dataBroker = dataBroker;
   }
  
   public void init() {
       setToasterStatusUp(null);
   }
 
   /**
    * Implemented from the AutoCloseable interface.
    */
   @Override
   public void close() {
       if (dataBroker != null) {
           WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
           tx.delete(OPERATIONAL,TOASTER_IID);
           Futures.addCallback(tx.submit(), new FutureCallback<Void>() {
               @Override
               public void onSuccess( final Void result ) {
                   LOG.debug("Delete Toaster commit result: " + result);
               }
 
               @Override
               public void onFailure( final Throwable failure) {
                   LOG.error("Delete of Toaster failed", failure);
               }
           } );
       }
   }
   
   private Toaster buildToaster( ToasterStatus status ) {
       // note - we are simulating a device whose manufacture and model are
       // fixed (embedded) into the hardware.
       // This is why the manufacture and model number are hardcoded.
       return new ToasterBuilder().setToasterManufacturer(TOASTER_MANUFACTURER).setToasterModelNumber(TOASTER_MODEL_NUMBER)
               .setToasterStatus( status ).build();
   }
 
   private void setToasterStatusUp( final Function<Boolean,Void> resultCallback ) {
       WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
       tx.put(OPERATIONAL,TOASTER_IID, buildToaster(ToasterStatus.Up));

       Futures.addCallback(tx.submit(), new FutureCallback<Void>() {
           @Override
           public void onSuccess(final Void result) {
               notifyCallback(true);
           }

           @Override
           public void onFailure(final Throwable failure) {
               // We shouldn't get an OptimisticLockFailedException (or any ex) as no
               // other component should be updating the operational state.
               LOG.error("Failed to update toaster status", failure);

               notifyCallback(false);
           }

           void notifyCallback(final boolean result) {
               if (resultCallback != null) {
                   resultCallback.apply(result);
               }
           }
       });
   }
}

In the init method, it creates a write-only transaction to write the ToasterStatus to Up in the operational data store. On close, it deletes the operational Toaster data.

Wiring the OpendaylightToaster service

We've implemented the toaster provider service - now we have to get our OpendaylightToaster instantiated and wired up with the MD-SAL. We use Blueprint which is an OSGi compendium spec for a dependency injection framework designed specifically for use in an OSGi container. Karaf includes the Apache Aries blueprint implementation.

To use blueprint, a bundle provides an XML file that describes what OSGi service dependencies are needed and what Java objects to instantiate.

Define the Toaster blueprint XML file

Next we'll define the toaster-provider.xml file under the src/main/resources/org/opendaylight/blueprint folder:

<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0" 
                  xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0
        "odl:use-default-for-reference-types="true">
  <!-- 
       "use-default-for-reference-types" is an ODL extension attribute that adds a filter to all services
       imported via "reference" elements where the "type" property is either not set or set to "default" if
       the odl:type attribute isn't explicitly specified. This ensures the default implementation is imported
       if there are other implementations advertised with other types.
  -->
 
  <!-- Import MD-SAL services.  -->
 
  <reference id="dataBroker" interface="org.opendaylight.controller.md.sal.binding.api.DataBroker" />
 
  <!-- Create the OpendaylightToaster instance and inject its dependencies -->
  <bean id="toaster" class="org.opendaylight.controller.sample.toaster.provider.OpendaylightToaster"
          init-method="init" destroy-method="close">
    <property name="dataBroker" ref="dataBroker"/>
  </bean>
</blueprint>

See https://wiki.opendaylight.org/view/Using_Blueprint for more in depth information on writing blueprint XML files.

Getting the Operational Status of the Toaster

First you need to run an Opendaylight karaf distro. You can obtain a released distro artifact from the Opendaylight nexus site. Or you can build a distro in the controller project under controller/karaf/opendaylight-karaf. After running ../bin/karaf, install the RESTCONF and toaster features:

  feature:install odl-restconf
  feature:install odl-toaster

To get the operational status of the toaster you will make a call to the RESTCONF service provided by MD-SAL. You do this by performing a GET to the operational data store.

  HTTP Method: GET
  HTTP URL: http://localhost:8181/restconf/operational/toaster:toaster

You should see the following response:

{
    toaster: {
        toasterManufacturer: "Opendaylight"
        toasterModelNumber: "Model 1 - Binding Aware"
        toasterStatus: "Up"
   }
}

Note: If you want XML instead of json, add Accept: application/yang.data+xml to the headers of the request.

How Does MD-SAL Know about my Toaster?

The sample-toaster bundle only defines a yang file and has no bundle Activator and has no code other than the generated source files. If you are wondering how MD-SAL becomes aware of the toaster yang data model then read on.

The magic is done via some files that are generated by the yang-maven-plugin under target/classes/META-INF that get inserted into the sample-toaster bundle.

  • The src/main/yang/toaster.yang file is copied to target/classes/META-INF/yang/toaster.yang.
  • The org.opendaylight.yangtools.yang.binding.YangModelBindingProvider file is generated in target/classes/META-INF/services and contains the fully-qualfied name of the toaster's generated $YangModelBindingProvider class. The MD-SAL's ModuleInfoBundleTracker class scrapes the META-INF/services/org.opendaylight.yangtools.yang.binding.YangModelBindingProvider resource from bundles on startup and reads the class name(s) defined in the file. For each YangModelBindingProvider class specified, the MD-SAL creates an instance and calls getModuleInfo() to return the singleton $YangModuleInfoImpl instance. This class has methods to obtain static configuration information about the yang module, e.g. name, revision, imports etc, as well as a getModuleSourceStream() method that provides an input stream to the META-INF/yang/toaster.yang file. Once the MD-SAL knows about a yang module and its definitions, it can wire it up to RestConf and other parts of the system.

Part 2: Enabling Remote Procedure Calls (RPC) - Lets make some toast!

Part 2 of the toaster example will add some behavior to the toaster. Having a toaster is cool but we'd really like it to make some toast for us. To accomplish this, we will define an RPC (Remote Procedure Call) in the toaster yang data model and write an implementation.

Define the yang RPC

Edit the existing toaster.yang file, where we will define 2 RPC methods, make-toast and cancel-toast (add the bold lines under the module toaster heading):

   module toaster {
       ... 
   //This defines a Remote Procedure Call (rpc). RPC provide the ability to initiate an action
   //on the data model. In this case the initating action takes two optional inputs (because default value is defined)
   //QUESTION: Am I correct that the inputs are optional because they have defaults defined? The REST call doesn't seem to account for this.
   rpc make-toast {
     description
       "Make some toast. The toastDone notification will be sent when the toast is finished.
        An 'in-use' error will be returned if toast is already being made. A 'resource-denied' error will 
        be returned if the toaster service is disabled.";
input { leaf toasterDoneness { type uint32 { range "1 .. 10"; } default '5'; description "This variable controls how well-done is the ensuing toast. It should be on a scale of 1 to 10. Toast made at 10 generally is considered unfit for human consumption; toast made at 1 is warmed lightly."; }
leaf toasterToastType { type identityref { base toast:toast-type; } default 'wheat-bread'; description "This variable informs the toaster of the type of material that is being toasted. The toaster uses this information, combined with toasterDoneness, to compute for how long the material must be toasted to achieve the required doneness."; } } } // rpc make-toast
// action to cancel making toast - takes no input parameters rpc cancel-toast { description "Stop making toast, if any is being made. A 'resource-denied' error will be returned if the toaster service is disabled."; } // rpc cancel-toast ... }


Running 'mvn clean install', we see the following additional classes generated:

  • ToasterService - an interface that extends RpcService and defines the RPC methods corresponding to the yang data model.
  • MakeToastInput - an interface defining a DTO providing the input parameters for the make-toast call.
  • MakeToastInputBuilder - a concrete class for creating MakeToastInput instances.

Note: It is important that you run the mvn clean stage everytime you modify the yang files. There are some files that are not generated if they already exist, which can lead to incorrect generated files. When you change .yang file, you should always run mvn clean, which will remove all of the generated yang files, via the mvn-clean-plugin defined in the common.opendaylight pom.xml file.

Implement the RPC methods

We've defined the data model interface for the RPC calls - now we must provide the implementation. We are going to modify our OpendaylightToaster class to implement the new ToasterService interface that was just generated. Only the relevant parts of the code are shown for simplicity:

public class OpendaylightToaster implements ToasterService, AutoCloseable {
 
  ...  
  private final ExecutorService executor;
  
  // The following holds the Future for the current make toast task.
  // This is used to cancel the current toast.
  private final AtomicReference<Future<?>> currentMakeToastTask = new AtomicReference<>();
  
  public OpendaylightToaster() {
      executor = Executors.newFixedThreadPool(1);
  }
   
  /**
  * Implemented from the AutoCloseable interface.
  */
  @Override
  public void close() {
      // When we close this service we need to shutdown our executor!
      executor.shutdown();
      
      ...
  }
  
  @Override
  public Future<RpcResult<Void>> cancelToast() {
      Future<?> current = currentMakeToastTask.getAndSet(null);
       if (current != null) {
           current.cancel(true);
       }

       // Always return success from the cancel toast call
       return Futures.immediateFuture(RpcResultBuilder.<Void> success().build());
  }
    
  @Override
  public Future<RpcResult<Void>> makeToast(final MakeToastInput input) {
       final SettableFuture<RpcResult<Void>> futureResult = SettableFuture.create();

       checkStatusAndMakeToast(input, futureResult, toasterAppConfig.getMaxMakeToastTries());

       return futureResult;
  }
 
  private void checkStatusAndMakeToast(final MakeToastInput input, final SettableFuture<RpcResult<Void>> futureResult,
           final int tries) {
       // Read the ToasterStatus and, if currently Up, try to write the status to Down.
       // If that succeeds, then we essentially have an exclusive lock and can proceed
       // to make toast.
       final ReadWriteTransaction tx = dataBroker.newReadWriteTransaction();
       ListenableFuture<Optional<Toaster>> readFuture = tx.read(OPERATIONAL, TOASTER_IID);

       final ListenableFuture<Void> commitFuture =
           Futures.transform(readFuture, (AsyncFunction<Optional<Toaster>, Void>) toasterData -> {
               ToasterStatus toasterStatus = ToasterStatus.Up;
               if (toasterData.isPresent()) {
                   toasterStatus = toasterData.get().getToasterStatus();
               }

               LOG.debug("Read toaster status: {}", toasterStatus);

               if (toasterStatus == ToasterStatus.Up) {
                   LOG.debug("Setting Toaster status to Down");

                   // We're not currently making toast - try to update the status to Down
                   // to indicate we're going to make toast. This acts as a lock to prevent
                   // concurrent toasting.
                   tx.put(OPERATIONAL, TOASTER_IID, buildToaster(ToasterStatus.Down));
                   return tx.submit();
               }

               LOG.debug("Oops - already making toast!");

               // Return an error since we are already making toast. This will get
               // propagated to the commitFuture below which will interpret the null
               // TransactionStatus in the RpcResult as an error condition.
               return Futures.immediateFailedCheckedFuture(
                       new TransactionCommitFailedException("", makeToasterInUseError()));
           });

       Futures.addCallback(commitFuture, new FutureCallback<Void>() {
           @Override
           public void onSuccess(final Void result) {
               // OK to make toast
               currentMakeToastTask.set(executor.submit(new MakeToastTask(input, futureResult)));
           }

           @Override
           public void onFailure(final Throwable ex) {
               if (ex instanceof OptimisticLockFailedException) {

                   // Another thread is likely trying to make toast simultaneously and updated the
                   // status before us. Try reading the status again - if another make toast is
                   // now in progress, we should get ToasterStatus.Down and fail.

                   if (tries - 1 > 0) {
                       LOG.debug("Got OptimisticLockFailedException - trying again");
                       checkStatusAndMakeToast(input, futureResult, tries - 1);
                   } else {
                       futureResult.set(RpcResultBuilder.<Void>failed()
                               .withError(ErrorType.APPLICATION, ex.getMessage()).build());
                   }
               } else if (ex instanceof TransactionCommitFailedException) {
                   LOG.debug("Failed to commit Toaster status", ex);

                   // Probably already making toast.
                   futureResult.set(RpcResultBuilder.<Void>failed()
                           .withRpcErrors(((TransactionCommitFailedException)ex).getErrorList()).build());
               } else {
                   LOG.debug("Unexpected error committing Toaster status", ex);
                   futureResult.set(RpcResultBuilder.<Void>failed().withError(ErrorType.APPLICATION,
                           "Unexpected error committing Toaster status", ex).build());
               }
           }
       });
   }
   
  private class MakeToastTask implements Callable<Void> {
      final MakeToastInput toastRequest;
      final SettableFuture<RpcResult<Void>> futureResult;
 
      public MakeToastTask(final MakeToastInput toastRequest,
                            final SettableFuture<RpcResult<Void>> futureResult ) {
          this.toastRequest = toastRequest;
          this.futureResult = futureResult;
      }
 
      @Override
      public Void call() {
          try {
              // make toast just sleeps for n seconds.
              Thread.sleep(toastRequest.getToasterDoneness());
          } catch (InterruptedException e) {
              LOG.info ("Interrupted while making the toast");
          }
 
          // Set the Toaster status back to up - this essentially releases the toasting lock.
          // We can't clear the current toast task nor set the Future result until the
          // update has been committed so we pass a callback to be notified on completion.
 
          setToasterStatusUp(result -> {
               currentMakeToastTask.set(null);
               LOG.debug("Toast done");
               futureResult.set(RpcResultBuilder.<Void>success().build());
               return null;
           });

          return null;
     }
}

In the above code you can see that we have implemented the makeToast and cancelToast methods, in addition to the close method from the AutoCloseable interface to ensure that we properly clean up our embedded thread executor. The basic workflow of makeToast is:

  1. Read the current ToasterStatus and ensure it's Up
  2. Write the ToasterStatus to Down
  3. Submit a MakeToastTask to the thread executor that simply sleeps for a period based on the desired doneness
  4. Write the ToasterStatus to Up


Refer to inline comments for more details on what is happening.

Register OpendaylightToaster with the RPC service

The next step is to register our OpendaylightToaster as the provider for the RPC calls. This is easily done by using the ODL rpc-implementation blueprint extension:

 <blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0" 
                  xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0
        "odl:use-default-for-reference-types="true">
   ...
   <!-- Create the OpendaylightToaster instance and inject its dependencies -->
   <bean id="toaster" class="org.opendaylight.controller.sample.toaster.provider.OpendaylightToaster"
     ...
   </bean>
 
   <!-- Register the OpendaylightToaster instance as an RPC implementation provider. The "rpc-implementation"
       element automatically figures out the RpcService interface although it can be explicitly specified.
   -->
   <odl:rpc-implementation ref="toaster"/>
 </blueprint>

Invoke make-toast via RestConf

It's finally time to make some delicious wheat toast! To invoke the make-toast via the Restconf you will perform an HTTP POST to an operations URL.

HTTP Method => POST
URL => http://localhost:8080/restconf/operations/toaster:make-toast 
Header =>   Content-Type: application/yang.data+json  
Body =>  
{
  "input" :
  {
     "toaster:toasterDoneness" : "10",
     "toaster:toasterToastType":"wheat-bread" 
  }
}

Note: The default and mandatory flags are not currently implemented in, so even though the toast type and doneness is defaulted in the yang model, you still have to provide their values here.

The XML equivalent to the above JSON example is shown below.

HTTP Method => POST
URL => http://localhost:8181/restconf/operations/toaster:make-toast
HEADER => Content-Type: application/xml
Body =>
<input xmlns="http://netconfcentral.org/ns/toaster">
     <toasterDoneness>10</toasterDoneness>
     <toasterToastType>wheat-bread</toasterToastType>
</input>

Note: An XML namespace is required in Lithium and above.

Invoke cancel-toast via RestConf

If you don't like burnt toast, you may want to cancel the make-toast operation part of the way through! You do this by invoking the cancel-toast call via restconf:

URL => http://localhost:8080/restconf/operations/toaster:cancel-toast
HTTP Method => POST

Note: There is a bug in the way the RestconfImpl class processes / routes the REST requests. If you define the Content-Type header, then the rest call is routed to a method which expects a non-empty body. In this case though we don't have any input, so our body should be empty. Thus an exception is thrown. In order to make the cancel-toast call work successfully, you need to invoke the above call, with NO content-type define. By doing that you route the request to a different method, which expects an empty body.

See the Toaster status updated

To see the updated toaster status, invoke the make-toast call (with a doneness of 10 to get the longest delay) and then immediately invoke the get to retrieve the Operational Status of the Toaster. You should now see: {

   toaster: {
       toasterManufacturer: "Opendaylight"
       toasterModelNumber: "Model 1 - Binding Aware"
       toasterStatus: "Down"
  }

}

Part 3: Add some configuration data - My toast is too light!

In part 3 we will explore defining and enabling configuration attributes (as opposed to operational attributes) in our yang toaster file. In this section we are going to define a new configuration attribute on the toaster which will allow the user to modify number of seconds each level of doneness will take. More importantly, we will illustrate how our OpendaylightToaster can register for changes in that configuration data as well as how the user can set, update and delete that information.

Add the configuration attribute to toaster.yang

The first step is to add a new attribute, darknessFactor, to the toaster container in the toaster.yang file.

 container toaster {
     ...
    
     leaf darknessFactor {
       type uint32;
       config true;
       default 1000;
       description
         "The darkness factor. Basically, the number of ms to multiple the doneness value by.";
     }
    
     ...
 }


Now run 'mvn clean install' to generate the updated Toaster interface.

Listening for Changes

In order for our OpendaylightToaster to get notified when the configuration data changes we need to implement the DataTreeChangeListener interface.

  ...
  import org.opendaylight.controller.md.sal.binding.api.DataTreeChangeListener;
  ...
  public class OpendaylightToaster implements ToasterService,  DataTreeChangeListener<Toaster>, AutoCloseable {
     ...
     @Override
     public void onDataTreeChanged(Collection<DataTreeModification<Toaster>> changes) {
             //TODO - implement
     }
     ...
  }


The DataTreeChangeListener interface has a single method, onDataTreeChanged, which passes a list of DataTreeModification events. The DataTreeModification events contain changes for the type of node that we registered for, in this case Toaster. The DataObjectModification instance, obtained from the DataTreeModification, contains the type of modification, one of WRITE, DELETE, or SUBTREE_MODIFIED, and the before and after data. Since the Toaster does not have any child containers or lists, SUBTREE_MODIFIED does not apply. The DataTreeModification also contains the InstanceIdentifier path from the root for the changed object.

The next step is to extract the updated data from the change event. Do this by providing the following implementation for the onDataTreeChanged method.

  ...
  //Thread safe holder for our darkness multiplier.
  private AtomicLong darknessFactor = new AtomicLong( 1000 );
  ...
  @Override
   public void onDataTreeChanged(Collection<DataTreeModification<Toaster>> changes) {
       for(DataTreeModification<Toaster> change: changes) {
           DataObjectModification<Toaster> rootNode = change.getRootNode();
           if(rootNode.getModificationType() == DataObjectModification.ModificationType.WRITE) {
               Toaster oldToaster = rootNode.getDataBefore();
               Toaster newToaster = rootNode.getDataAfter();
               LOG.info("onDataTreeChanged - Toaster config with path {} was added or replaced: old Toaster: {}, new Toaster: {}",
                       change.getRootPath().getRootIdentifier(), oldToaster, newToaster);

               Long darkness = newToaster.getDarknessFactor();
               if(darkness != null) {
                   darknessFactor.set(darkness);
               }
           } else if(rootNode.getModificationType() == DataObjectModification.ModificationType.DELETE) {
               LOG.info("onDataTreeChanged - Toaster config with path {} was deleted: old Toaster: {}",
                       change.getRootPath().getRootIdentifier(), rootNode.getDataBefore());
           }
       }
   }
   ...

The last step is to modify the MakeToastTask call method to use our new darkness factor instead of a hard-coded value.

 private class MakeToastTask implements Callable<Void> {
       ...
       @Override
      public Void call() throws InterruptedException {
          try
          {
              // make toast just sleeps for n seconds per doneness level.
              Thread.sleep(OpendaylightToaster.this.darknessFactor.get() * toastRequest.getToasterDoneness());
          }
          catch( InterruptedException e ) {
              ...
          }
          ...
      }
   }

The final step is to register our listener with the DataBroker service in order to receive the notifications. We will perform this registration in the init method:

  private ListenerRegistration<OpendaylightToaster> dataTreeChangeListenerRegistration;
  ...
  public void init() {
       dataTreeChangeListenerRegistration = dataBroker.registerDataTreeChangeListener(
               new DataTreeIdentifier<>(CONFIGURATION, TOASTER_IID), this);
       ...
   }
   ...
   public void close() {
       ...
       if (dataTreeChangeListenerRegistration != null) {
           dataTreeChangeListenerRegistration.close();
       }
       ...
   }

We have now registered our toaster as a listener for changes to the toaster node and any node below it.

Changing the Darkness Factor

To change the darkness factor we will use a REST call to the restconf service provided by MD-SAL. Once your controller is started, perform the following PUT:

  HTTP Method: PUT
  URL:  http://localhost:8080/restconf/config/toaster:toaster
  HEADER: content-type: application/yang.data+json
  BODY: 
  {
    toaster:
    {
       darknessFactor: "2000"
    }
 }

You should receive a return code of 200. If you perform a GET to the same URL, your should see the update darkness factor returned. At this point, if you perform the make-toast RPC call you should see the delay reflect the value of the darknessFactor * the doneness.

Part 4: Add state data to the ToasterService implementation (JMX Access) - Count my toast!

For internal statistical purposes and troubleshooting, we'd like to keep track of how many pieces of toast the toaster has made over time. We need an attribute, ToastsMade, to track the count and a way to obtain the count. Whenever we make-toast, we want to increment ToastsMade. In addition, we'd like a mechanism to clear the ToastsMade count.

To accomplish this, we'll define internal state data and an RPC call in an MXBean that is only accessible via JMX.

Define the MXBean interface

We'll define an MXBean interface with methods to get the ToastsMade count and an RPC call to clear the count:

    public interface ToasterProviderRuntimeMXBean {
        Long getToastsMade();
 
        void clearToastsMade();
    }

Implement the MXBean interface

Now that we've defined the MXBean interface for our state data and behavior we need to provide an implementation. Since the OpenDaylightToaster makes toast, we'll implement it there.

The ToasterProviderRuntimeMXBean provides the interface for access to the state data so we need to modify OpenDaylightToaster to implement the ToasterProviderRuntimeMXBean interface:

public class OpendaylightToaster implements ..., ToasterProviderRuntimeMXBean, ... {
   ...
   private final AtomicLong toastsMade = new AtomicLong(0);
   ...
   
/** * JMX RPC call implemented from the ToasterProviderRuntimeMXBean interface. */ @Override public void clearToastsMade() { LOG.info( "clearToastsMade" ); toastsMade.set(0); }
/** * Accessor method implemented from the ToasterProviderRuntimeMXBean interface. */ @Override public Long getToastsMade() { return toastsMade.get(); } ...
private class MakeToastTask implements Callable<Void> { ... @Override public Void call() throws InterruptedException { ... toastsMade.incrementAndGet(); ... } } }

Register the ToasterProviderRuntimeMXBean implementation

We need to do a final step to register the OpendaylightToaster as the ToasterProviderRuntimeMXBean implementation with the platform JMX server. We do this by extending the AbstractMXBean base class and invoke the register method in init. Conversely we invoke unregister in close':

  public class OpendaylightToaster extends AbstractMXBean ... {
      ...
      public OpendaylightToaster(...) {
          super("OpendaylightToaster", "toaster-provider", null);
          ...
      }
      ...
      public void init() {
          ...
          // Register our MXBean.
          register();
      }

      public void close() {
          // Unregister our MXBean.
          unregister();
          ...
      }
      ...
  }

Accessing toastsMade and clearToastsMade via JMX

The ToastsMade attribute that we added is available via MBeans through the java management beans. You can programmatically access these through the mbean platform or via JConsole.

  • JConsole is a utility shipped with each JDK and is located in the bin directory of your java home folder.

Launch JConsole, double click on the jconsole application under

$JAVA_HOME/bin/jconsole

Note: Path may change based on OS and installation

  • Connect to the local running controller process either by selecting the org.apache.karaf.main.Main under Local Process.
  • Once connected, navigate to the "MBeans" tab.
  • Expand the "org.opendaylight.controller->toaster-provider->OpendaylightToaster" nodes.
  • Select "Attributes". You will now see the ToastsMade attribute displayed and this attribute will change when the make-toast RPC call is executed.
  • After you have invoked make-toast call a few times, refresh the attributes and see that the value increased.
  • Now select "Operations", and click the clearToastsMade button.
  • Return to the Attributes and note that the counter is now set to 0.

Part 5: Add a consumer of the ToasterService - Let's make breakfast!

We've seen how we can use RestConf to access the ToasterService RPC methods. In this section we'll show how to access the ToasterService programmatically from within the controller.

We'll create a new service called KitchenService that provides a method to make breakfast (this is located in the sample-toaster-consumer project). This service will access the ToasterService to provide the toast for our breakfast.

The KitchenService defines a higher-level service for making a full breakfast. This nicely demonstrates “service chaining”, where a consumer of one or more services is also a provider of another service. This example will only call into the 'toast' service but one can see that it could be extended to also call into an 'eggs' service and also add a 'coffee' service etc.

Define the KitchenService interface

For the sake of brevity, we'll hand-code the KitchenService data model and interface instead of defining it in yang. In a true kitchenService model you would likely want to define the KitchenService in yang to get the benefit of auto-generated classes and the out-of-box functionality that MD-SAL provides. For this example, we define an enumeration and interface java files under src/main/java, in the org.opendaylight.controller.sample.kitchen.api package.

 //EggsType.java  
 public enum EggsType {
     SCRAMBLED,
     OVER_EASY,
     POACHED
 }
 //KitchenService.java 
 public interface KitchenService {
   
     Future<RpcResult<Void>> makeBreakfast( EggsType eggs, Class<? extends ToastType> toast, int toastDoneness );
    
 }

Our breakfast only includes eggs with the toast for simplicity - a complete breakfast may also include bacon or sausage and coffee. Eggs, breakfast meat, coffee etc could also be separate data models with corresponding services like the ToasterService - we leave that as an exercise for the reader.

Define the KitchenService implementation

Next we create a class, KitchenServiceImp, to implement the interface and access the ToasterService to make the toast:

public class KitchenServiceImpl implements KitchenService {

    private static final Logger log = LoggerFactory.getLogger( KitchenServiceImpl.class );

    private final ToasterService toaster;

    private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());

    public KitchenServiceImpl(ToasterService toaster) {
        this.toaster = toaster;
    }

    @Override
    public Future<RpcResult<Void>> makeBreakfast(EggsType eggsType, Class<? extends ToastType> toastType,
            int toastDoneness) {
        // Call makeToast and use JdkFutureAdapters to convert the Future to a ListenableFuture, The
        // OpendaylightToaster impl already returns a ListenableFuture so the conversion is actually a no-op.

        ListenableFuture<RpcResult<Void>> makeToastFuture = JdkFutureAdapters
                .listenInPoolThread(makeToast(toastType, toastDoneness), executor);

        ListenableFuture<RpcResult<Void>> makeEggsFuture = makeEggs(eggsType);

        // Combine the 2 ListenableFutures into 1 containing a list RpcResults.

        ListenableFuture<List<RpcResult<Void>>> combinedFutures = Futures
                .allAsList(ImmutableList.of(makeToastFuture, makeEggsFuture));

        // Then transform the RpcResults into 1.

        return Futures.transform(combinedFutures,
            (AsyncFunction<List<RpcResult<Void>>, RpcResult<Void>>) results -> {
                boolean atLeastOneSucceeded = false;
                Builder<RpcError> errorList = ImmutableList.builder();
                for (RpcResult<Void> result : results) {
                    if (result.isSuccessful()) {
                        atLeastOneSucceeded = true;
                    }

                    if (result.getErrors() != null) {
                        errorList.addAll(result.getErrors());
                    }
                }

                return Futures.immediateFuture(RpcResultBuilder.<Void>status(atLeastOneSucceeded)
                        .withRpcErrors(errorList.build()).build());
            });
    }
  
    private ListenableFuture<RpcResult<Void>> makeEggs(EggsType eggsType) {
        return executor.submit(() -> RpcResultBuilder.<Void>success().build());
    }
  
    private Future<RpcResult<Void>> makeToast( Class<? extends ToastType> toastType,
                                               int toastDoneness ) {
        // Access the ToasterService to make the toast.
  
        MakeToastInput toastInput = new MakeToastInputBuilder().setToasterDoneness((long) toastDoneness)
                .setToasterToastType(toastType).build();

        return toaster.makeToast(toastInput);
    }
}

Wiring the KitchenService implementation

Similar to the toaster provider service, we'll use blueprint to wire it up. We define a toaster-consumer.xml file under src/main/resources/org/opendaylight/blueprint:

<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
           xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
    odl:use-default-for-reference-types="true">
 
  <!-- Retrieves the RPC service for the ToasterService interface -->
  <odl:rpc-service id="toasterService" interface="org.opendaylight.yang.gen.v1.http.netconfcentral.org.ns.toaster.rev091120.ToasterService"/>
 
  <!-- Create the KitchenServiceImpl instance and inject the RPC service identified by "toasterService" -->
  <bean id="kitchenService" class="org.opendaylight.controller.sample.kitchen.impl.KitchenServiceImpl">
    <argument ref="toasterService"/>
  </bean>
 
  <!-- Advertise the KitchenServiceImpl with the OSGi registry with the type property set to "default" . The
       type property is optional but can be used to distinguish this implementation from any other potential
       KitchenService implementations (if there were any). Clients consuming the KitchenService can pick the
       desired implementation via the particular type.
  -->
  <service ref="kitchenService" interface="org.opendaylight.controller.sample.kitchen.api.KitchenService"
          odl:type="default"/>
</blueprint>

Add JMX RPC to make breakfast

At this point, if we deployed the kitchen service we wouldn't be able to access it via restconf as we didn't define a yang data model for it. Presumably, for a real service, there would be java clients to consume it. In lieu of that we can utilize JMX to exercise the kitchen service to make breakfast, similar as we did earlier for the clearToastsMade RPC in the toaster provider.

We'll add a makeScrambledWithWheat RPC. First define the KitchenServiceRuntimeMXBean interface:

  public interface KitchenServiceRuntimeMXBean {
      Boolean makeScrambledWithWheat();
  }

Next we modify the KitchenServiceImpl to implement the interface and extend from AbstractMXBean:

   public class KitchenServiceImpl extends AbstractMXBean
        implements KitchenService, KitchenServiceRuntimeMXBean {
   ...
   public KitchenServiceImpl(ToasterService toaster) {
       super("KitchenService", "toaster-consumer", null);
       this.toaster = toaster;
   }
   ...
   @Override
   public Boolean makeScrambledWithWheat() {
       try {
           // This call has to block since we must return a result to the JMX client.
           RpcResult<Void> result = makeBreakfast(EggsType.SCRAMBLED, WheatBread.class, 2).get();
           if (result.isSuccessful()) {
               LOG.info("makeBreakfast succeeded");
           } else {
               LOG.warn("makeBreakfast failed: " + result.getErrors());
           }

           return result.isSuccessful();

       } catch (InterruptedException | ExecutionException e) {
           LOG.warn("An error occurred while maing breakfast: " + e);
       }

       return Boolean.FALSE;
   }

In the toaster-consumer.xml blueprint, add init-method/destroy-method definitions to register/unregister the MXBean:

 <blueprint ...>
   ...
   <bean id="kitchenService" class="org.opendaylight.controller.sample.kitchen.impl.KitchenServiceImpl"
         init-method="register" destroy-method="unregister">
      <argument ref="toasterService"/>
   </bean>
   ...
 </blueprint>

Make breakfast via JMX

We can access the kitchen-service MBean via JConsole as we did earlier with the toaster-service MBean.

  • Navigate to the MBeans tab
  • Expand the org.opendaylight.controller->toaster-consumer->KitchenService->Operations node.
  • Click the makeScrambledWithWheat button.
  • To verify it actually made the toast, expand org.opendaylight.controller->toaster-provider->OpendaylightToaster->Attributes and check the value of ToastsMade.

Part 6: Notifications - Oh no, the Toaster is out of bread!

This part will make use of the MD-SAL's unsolicited notification service to have the OpenDaylightToaster send notifications when significant events occur. Notifications can be consumed by registered listener implementations or by external netconf clients.

A toaster can only make toast if it has a supply of bread. Currently, our OpenDaylightToaster has an infinite supply of bread which isn't very realistic in the real world.

We'll modify the OpenDaylightToaster to have a finite stock of bread. We'll keep it simple and maintain an overall limit encompassing all types of bread instead of a limit per bread type.

When called to make toast, if out of bread, a toasterOutOfBread notification will be sent.

We'll also add an RPC call, restock-toaster, that can be used to set the amount of bread in stock. In addition it will send a toasterRestocked notification.

The KitchenService will register for both notifications and act accordingly when received.

Define the notifications and RPC

We'll define the 2 notifications and RPC in the toaster.yang file.

module toaster {
   ... 
   rpc restock-toaster {
       description
         "Restocks the toaster with the amount of bread specified.";
       
input { leaf amountOfBreadToStock { type uint32; description "Indicates the amount of bread to re-stock"; } } }
notification toasterOutOfBread { description "Indicates that the toaster has run of out bread."; } // notification toasterOutOfStock
notification toasterRestocked { description "Indicates that the toaster has run of out bread."; leaf amountOfBread { type uint32; description "Indicates the amount of bread that was re-stocked"; } } // notification toasterRestocked } // module toaster


After running 'mvn clean install', several new classes will be generated:

  • ToasterOutOfBread - an interface defining a DTO for the toasterOutOfBread notification.
  • ToasterOutOfBreadBuilder - a concrete class for creating ToasterOutOfBread instances.
  • ToasterRestocked - an interface defining a DTO for the toasterRestocked notification.
  • ToasterRestockedBuilder - a concrete class for creating ToasterRestocked instances.
  • ToasterListener - interface for consumers of the toaster notifications to implement that defines receipt methods for each notification type.

Implement notifications and RPC in OpenDaylightToaster

Next we add code to the OpenDaylightToaster to keep track of the amount of bread in stock, implement the restockToaster RPC and to send the notifications.

public class OpendaylightToaster ... {
   ...
   private NotificationProviderService notificationProvider;
   ...
   private final AtomicLong amountOfBreadInStock = new AtomicLong(100);
   ...
   public void setNotificationProvider(final NotificationPublishService notificationPublishService) {
       this.notificationProvider = notificationPublishService;
   }
   ...
   
private void checkStatusAndMakeToast( final MakeToastInput input, final SettableFuture<RpcResult<Void>> futureResult ) { ... final ListenableFuture<Void> commitFuture = Futures.transform(readFuture, (AsyncFunction<Optional<Toaster>, Void>) toasterData -> { ... if (toasterStatus == ToasterStatus.Up) { if( outOfBread() ) { LOG.debug( "Toaster is out of bread" ); return Futures.immediateFailedCheckedFuture( new TransactionCommitFailedException("", makeToasterOutOfBreadError())); } ... } ... } } ); ... } ... /** * RestConf RPC call implemented from the ToasterService interface. * Restocks the bread for the toaster, resets the toastsMade counter to 0, and sends a * ToasterRestocked notification. */ @Override public Future<RpcResult<java.lang.Void>> restockToaster(final RestockToasterInput input) { LOG.info("restockToaster: " + input); amountOfBreadInStock.set(input.getAmountOfBreadToStock()); if (amountOfBreadInStock.get() > 0) { ToasterRestocked reStockedNotification = new ToasterRestockedBuilder() .setAmountOfBread(input.getAmountOfBreadToStock()).build(); notificationProvider.offerNotification(reStockedNotification); } return Futures.immediateFuture(RpcResultBuilder.<Void>success().build()); } ... private boolean outOfBread() { return amountOfBreadInStock.get() == 0; }
private class MakeToastTask implements Callable<Void> { ... @Override public Void call() throws InterruptedException { ... amountOfBreadInStock.getAndDecrement(); if(outOfBread()) { LOG.info("Toaster is out of bread!"); notificationProvider.offerNotification(new ToasterOutOfBreadBuilder().build()); } ... } }

}

Wire the OpenDaylightToaster for notifications

The OpenDaylightToaster needs access to the MD-SAL's NotificationPublishService in order to send notifications. We need to import the OSGi service and inject into the OpenDaylightToaster in the toaster-provider.xml blueprint file:

<blueprint ...>
   ...
   <reference id="notificationService" interface="org.opendaylight.controller.md.sal.binding.api.NotificationPublishService"/>
  
   <bean id="toaster" class="org.opendaylight.controller.sample.toaster.provider.OpendaylightToaster"
            init-method="init" destroy-method="close">
      <property name="dataBroker" ref="dataBroker"/>
      <property name="notificationProvider" ref="notificationService"/>
    </bean>
    ...
</blueprint>

Implement notifications in KitchenServiceImpl

Next we modify the KitchenServiceImpl to implement the ToasterListener interface and the notification methods.

public class KitchenServiceImpl implements KitchenService, KitchenServiceRuntimeMXBean, ToasterListener {
   ...
   private volatile boolean toasterOutOfBread;
   
private Future<RpcResult<Void>> makeToast( Class<? extends ToastType> toastType, int toastDoneness ) { if (toasterOutOfBread) { LOG.info("We're out of toast but we can make eggs"); return Futures.immediateFuture(RpcResultBuilder.<Void>success().withWarning(ErrorType.APPLICATION, "partial-operation", "Toaster is out of bread but we can make you eggs").build()); } ... } ... /** * Implemented from the ToasterListener interface. */ @Override public void onToasterOutOfBread( ToasterOutOfBread notification ) { log.info( "ToasterOutOfBread notification" ); toasterOutOfBread = true; }
/** * Implemented from the ToasterListener interface. */ @Override public void onToasterRestocked( ToasterRestocked notification ) { log.info( "ToasterRestocked notification - amountOfBread: " + notification.getAmountOfBread() ); toasterOutOfBread = false; } }


The onToasterOutOfBread and onToasterRestocked notification methods simply set and clear the toasterOutOfBread. When called to make breakfast, if toasterOutOfBread, we can't make toast but attempt to make the eggs so someone can at least get something for breakfast.

Wire the KitchenServiceImpl for notifications

The KitchenServiceImpl needs to be registered with the MD-SAL's NotificationPublishService in order to receive notifications. We use the notification-listener ODL blueprint extension:

<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
          xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
   odl:use-default-for-reference-types="true">
    ...
   
   <odl:notification-listener ref="kitchenService"/>
   ...
</blueprint>

Testing the notifications

We'll first make the toaster run out of bread. The default amount of bread in stock is 100. Rather than taking the time to make 100 toasts, we'll first invoke the restock-toaster RPC via restconf to lower it to 3.

HTTP Method => POST
URL => http://localhost:8080/restconf/operations/toaster:restock-toaster 
Header => Content-Type: application/yang.data+json  
Body =>  
{
  "input" :
  {
     "toaster:amountOfBreadToStock" : "3"
  }
}

Next we'll make 3 breakfast orders to deplete the toaster's stock.

  • Open JConsole
  • Navigate to the MBeans tab
  • Expand the org.opendaylight.controller->toaster-consumer->KitchenService->Operations node.
  • Click the makeScrambledWithWheat button 3 times.

After the 3rd breakfast, the toaster will be out of bread and send the toasterOutOfBread notification to the kitchen service. You should see this message in the controller log/console:

   ...KitchenServiceImpl - ToasterOutOfBread notification

Click the makeScrambledWithWheat button again - the result should be true and you should see this message in the log:

   ...KitchenServiceImpl - We're out of toast but we can make eggs

Invoke restock-toaster again - you should see this message in the log:

   ...KitchenServiceImpl - ToasterRestocked notification - amountOfBread: 3

Part 7: Adding configuration for the Toaster provider application

In this part we illustrate how to define and implement user-facing configuration to initialize or augment the business logic for an application. We'll add configuration knobs so a user can change/set the manufacturer and model-number of the Toaster. The configuration will be defined in yang and stored and retrieved from the MD-SAL data store. We'll use the clustered-app-config blueprint extension to obtain the configuration data from the MD-SAL data store and inject into the OpendaylightToaster.

Define the toaster-app-config data model

First we define the yang data model in toaster-app-config.yang:

 module toaster-app-config {
     yang-version 1;
     namespace "urn:opendaylight:params:xml:ns:yang:controller:toaster-app-config";
     prefix toaster-app-config;

     import toaster { prefix toaster; revision-date 2009-11-20; }

     description
       "Configuration for the Opendaylight toaster application.";

     revision "2016-05-03" {
         description
             "Initial revision.";
     }

     container toaster-app-config {
         leaf manufacturer {
             type toaster:DisplayString;
             default "Opendaylight";
         }

         leaf model-number {
             type toaster:DisplayString;
             default "Model 1 - Binding Aware";
         }

         leaf max-make-toast-tries {
             type uint16;
             default 2;
         }
     }
 }

After generating the java source from the yang, the class of note is ToasterAppConfig.

Using the configuration in OpendaylightToaster

To initialize the OpendaylightToaster with the configuration data, we add a constructor that passes a ToasterAppConfig instance and stores it in a field:

 public class OpendaylightToaster ... {
     ...
     private final ToasterAppConfig toasterAppConfig;
     ...
     public OpendaylightToaster(ToasterAppConfig toasterAppConfig) {
         ...
         this.toasterAppConfig = toasterAppConfig;
     }
 }

When we build a Toaster instance, we use the data from the toasterAppConfig field:

  private Toaster buildToaster(final ToasterStatus status) {
     return new ToasterBuilder().setToasterManufacturer(toasterAppConfig.getManufacturer())
               .setToasterModelNumber(toasterAppConfig.getModelNumber()).setToasterStatus(status).build();
  }

Wiring ToasterAppConfig into the OpendaylightToaster

The next step is to read the ToasterAppConfig from the data store and wire it into the OpendaylightToaster constructor. We do this in the blueprint XML using the clustered-app-config extension element. In the toaster-provider.xml:

 <blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
           xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0">
    ...
    <!-- "clustered-app-config" is an ODL extension that obtains an application configuration yang container
         from the MD-SAL data store and makes the binding DataObject available as a bean that can be injected
         into other beans. Here we obtain the ToasterAppConfig container DataObject. This also shows how to
         specify default data via the "default-config" child element. While default leaf values defined in the
         yang are returned, one may have more complex data, eg lists, that require default data. The
         "default-config" must contain the XML representation of the yang data, including namespace, wrapped
         in a CDATA section to prevent the blueprint container from treating it as markup.
    -->
    <odl:clustered-app-config id="toasterAppConfig"
        binding-class="org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.toaster.app.config.rev160503.ToasterAppConfig">
       <odl:default-config><![CDATA[
          <toaster-app-config xmlns="urn:opendaylight:params:xml:ns:yang:controller:toaster-app-config">
             <max-make-toast-tries>3</max-make-toast-tries>
          </toaster-app-config>
       ]]></odl:default-config>
    </odl:clustered-app-config>
    ...
    <!-- Create the OpendaylightToaster instance and inject its dependencies -->
    <bean id="toaster" class="org.opendaylight.controller.sample.toaster.provider.OpendaylightToaster"
            init-method="register" destroy-method="unregister">
       <argument ref="toasterAppConfig"/>
       ...
   </bean>
   ...
 </blueprint>

The clustered-app-config element obtains the ToasterAppConfig data from the MD-SAL data store and makes it available as a bean named toasterAppConfig that is injected into the OpendaylightToaster constructor via the <argument> element. See the inline comments for more detail. In addition, if the toaster-app-config model is changed by a user via REST, the clustered-app-config is triggered and automatically restarts the blueprint container such that a new OpendaylightToaster is created with the updated ToasterAppConfig data.

For more information on the clustered-app-config extension, see Using the Datastore for Application Configuration.