lighty.io has been enabling OpenDaylight (ODL) components and applications to run in various environments and frameworks. Those components and applications have never been useable by ODL developers before. Spring.io is popular java ecosystem for application developers. Wouldn’t it be perfect to enable lighty.io for Spring as the target runtime environment is Java SE? This is exactly what we did. Lighty.io provides dependency injection extensions for Spring. That makes it easy to consume ODL core services via Spring’s dependency injection system. Let’s check how simple it is to create ODL based NETCONF SDN controller application in the spring-boot environment. The architecture of this example is in the picture.

1. Generate Spring Boot skeleton project

First, generate maven Spring Boot skeleton application with start.spring.io. In this guide we will use Spring Boot release 2.0.2.

For now we will need only standard web dependency, as we want to expose some REST endpoints.

Let’s use inputs as follows:

group id: io.lighty.core
artifact id: lighty-controller-springboot
name: lighty-controller-springboot
despcription: Demo lighty.io project for Spring Boot
package: io.lighty.core.controller.springboot
packaging: jar
java version: 8

Start.spring.io generates for us basic maven project hierarchy with application entry point in class LightyControllerSpringbootApplication.

2. lighty.io project setup

Next, we need to start lighty.io inside Spring, so just simply add lighty.io dependency injection extension for Spring to pom.xml.
This artifact is bean provider for Spring’s @Configuration.

<lighty.version>8.0.0-SNAPSHOT</lighty.version>

<dependency>
    <groupId>io.lighty.core</groupId>
    <artifactId>lighty-controller-spring-di</artifactId>
    <version>${lighty.version}</version>
</dependency>

And to use default single node lighty.io configuration add a dependency

<dependency>
    <groupId>io.lighty.resources</groupId>
    <artifactId>singlenode-configuration</artifactId>
    <version>${lighty.version}</version>
</dependency>

For the NETCONF devices, we will need also the NETCONF Southbound module

<dependency>
    <groupId>io.lighty.modules.southbound.netconf</groupId>
    <artifactId>lighty-netconf-sb</artifactId>
    <version>${lighty.version}</version>
</dependency>

Since now we have lighty-core on the classpath, let’s create lighty.io bean definitions for Spring.

Create new @Configuration class LightyConfiguration extending LightyCoreSpringConfiguration which initializes all the lighty.io services as Spring beans. To be able to initialize all lighty.io services it is neccessary to create lighty.io core controller as Spring bean, for example in class LightyConfiguration. In this example we need to initialize LightyController and NETCONF-SBPlugin too..

@Configuration
public class LightyConfiguration extends LightyCoreSpringConfiguration {

    private static final Logger LOG = LoggerFactory.getLogger(LightyConfiguration.class);

    @Bean
    LightyController initLightyController() throws Exception {
        LOG.info("Building LightyController Core");
        final LightyControllerBuilder lightyControllerBuilder = new LightyControllerBuilder();
        final Set<YangModuleInfo> mavenModelPaths = new HashSet<>();
    
        mavenModelPaths.addAll(NetconfConfigUtils.NETCONF_TOPOLOGY_MODELS);
        mavenModelPaths.add($YangModuleInfoImpl.getInstance()); //Add toaster model

        final LightyController lightyController = lightyControllerBuilder
            .from(ControllerConfigUtils.getDefaultSingleNodeConfiguration(mavenModelPaths))
            .build();
        LOG.info("Starting LightyController (waiting 10s after start)");
        final ListenableFuture<Boolean> started = lightyController.start();
        started.get();
        LOG.info("LightyController Core started");

        return lightyController;
    }

    @Bean
    NetconfSBPlugin initNetconfSBP(LightyController lightyController) throws ExecutionException, InterruptedException {
        final NetconfConfiguration netconfSBPConfiguration = NetconfConfigUtils.injectServicesToTopologyConfig(
            NetconfConfigUtils.createDefaultNetconfConfiguration(), lightyController.getServices());
        NetconfTopologyPluginBuilder netconfSBPBuilder = new NetconfTopologyPluginBuilder();
        final NetconfSBPlugin netconfSouthboundPlugin = netconfSBPBuilder
            .from(netconfSBPConfiguration, lightyController.getServices())
            .build();
        netconfSouthboundPlugin.start().get();
        return netconfSouthboundPlugin;
    }

}

From this point we have created bean definitions for Spring environment. Let’s add some basic logic.

3. Create basic lighty.io REST service

In this step, we will create simple REST service exposing endpoints for a simple example of managing NETCONF devices.
Let’s add endpoints which provide us functionality to

  • connect to a device
  • list all devices
  • disconnect device

Create simply @RestController class skeleton for REST endpoints returning some default responses.

@RestController
@RequestMapping(path = "netconf")
public class NetconfDeviceRestService {

    @GetMapping(path = "/list")
    public ResponseEntity getNetconfDevicesIds() {
        // TODO add endpoin impl
        return ResponseEntity.noContent().build();
    }

    @PutMapping(path = "/id/{netconfDeviceId}")
    public ResponseEntity connectNetconfDevice(@PathVariable final String netconfDeviceId,
                                               @RequestBody final NetconfDeviceRequest deviceInfo) {
        // TODO add endpoin impl
        return ResponseEntity.noContent().build();
    }

    @DeleteMapping(path = "/id/{netconfDeviceId}")
    public ResponseEntity disconnectNetconfDevice(@PathVariable final String netconfDeviceId) {
        // TODO add endpoin impl
        return ResponseEntity.noContent().build();
    }
}
  • GET /netconf/list will accept no attributes and returns all stored NETCONF devices
  • PUT /netconf/id/{netconfDeviceId} will need device ID and device details needed to connect contained in request body
  • DELETE /netconf/id/{netconfDeviceId} will disconnect device and removed it from datastore

All the lighty.io core services are already defined as beans so we can inject DataBroker and MountPointService into our REST Service class NetconfDeviceRestService, to provide access to ODL’s global datastore.

@Autowired
@Qualifier("BindingDataBroker")
private DataBroker dataBroker;

@Autowired
private MountPointService mountPointService;

Now we can write and read from datastore so let’s update endpoints implementations in class NetconfDeviceRestService:

private static final InstanceIdentifier<Topology> NETCONF_TOPOLOGY_IID = InstanceIdentifier
        .create(NetworkTopology.class)
        .child(Topology.class, new TopologyKey(new TopologyId("topology-netconf")));
private static final long TIMEOUT = 1;


@GetMapping(path = "/list")
public ResponseEntity getNetconfDevicesIds() throws InterruptedException, ExecutionException, TimeoutException {
    try (final ReadOnlyTransaction tx = dataBroker.newReadOnlyTransaction()) {
        final Optional<Topology> netconfTopoOptional =
            tx.read(LogicalDatastoreType.OPERATIONAL, NETCONF_TOPOLOGY_IID).get(TIMEOUT, TimeUnit.SECONDS);

        if (netconfTopoOptional.isPresent() && netconfTopoOptional.get().getNode() != null) {
            final List<NetconfDeviceResponse> response = new ArrayList<>();
            for (Node node : netconfTopoOptional.get().getNode()) {
                final NetconfDeviceResponse nodeResponse = NetconfDeviceResponse.from(node);
                response.add(nodeResponse);

                final Optional<MountPoint> netconfMountPoint =
                    mountPointService.getMountPoint(
                        NETCONF_TOPOLOGY_IID.child(Node.class, new NodeKey(node.getNodeId()))
                    );

                if (netconfMountPoint.isPresent()) {
                    final Optional<DataBroker> netconfDataBroker =
                        netconfMountPoint.get().getService(DataBroker.class);
                    if (netconfDataBroker.isPresent()) {
                        final ReadOnlyTransaction netconfReadTx =
                            netconfDataBroker.get().newReadOnlyTransaction();

                        final Optional<Toaster> toasterData = netconfReadTx
                            .read(LogicalDatastoreType.OPERATIONAL, TOASTER_IID).get(TIMEOUT, TimeUnit.SECONDS);

                        if (toasterData.isPresent() && toasterData.get().getDarknessFactor() != null) {
                            nodeResponse.setDarknessFactor(toasterData.get().getDarknessFactor());
                        }
                    }
                }
            }

            return ResponseEntity.ok(response);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

@PutMapping(path = "/id/{netconfDeviceId}")
public ResponseEntity connectNetconfDevice(@PathVariable final String netconfDeviceId,
                                           @RequestBody final NetconfDeviceRequest deviceInfo)
    throws InterruptedException, ExecutionException, TimeoutException {

    final WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
    final NodeId nodeId = new NodeId(netconfDeviceId);
    final InstanceIdentifier<Node> netconfDeviceIID = NETCONF_TOPOLOGY_IID
        .child(Node.class, new NodeKey(nodeId));

    final Node netconfDeviceData = new NodeBuilder()
        .setNodeId(nodeId)
        .addAugmentation(NetconfNode.class, new NetconfNodeBuilder()
            .setHost(new Host(new IpAddress(new Ipv4Address(deviceInfo.getAddress()))))
            .setPort(new PortNumber(deviceInfo.getPort()))
            .setCredentials(new LoginPasswordBuilder()
                    .setUsername(deviceInfo.getUsername())
                    .setPassword(deviceInfo.getPassword())
                    .build())
            .setTcpOnly(false)
            .build())
        .build();
    tx.put(LogicalDatastoreType.CONFIGURATION, netconfDeviceIID, netconfDeviceData);

    tx.submit().get(TIMEOUT, TimeUnit.SECONDS);

    return ResponseEntity.ok().build();
}

@DeleteMapping(path = "/id/{netconfDeviceId}")
public ResponseEntity disconnectNetconfDevice(@PathVariable final String netconfDeviceId)
    throws InterruptedException, ExecutionException, TimeoutException {

    final WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
    final NodeId nodeId = new NodeId(netconfDeviceId);
    final InstanceIdentifier<Node> netconfDeviceIID = NETCONF_TOPOLOGY_IID
        .child(Node.class, new NodeKey(nodeId));

    tx.delete(LogicalDatastoreType.CONFIGURATION, netconfDeviceIID);

    tx.submit().get(TIMEOUT, TimeUnit.SECONDS);

    return ResponseEntity.ok().build();
}

Be sure you added device yang model to PROJECT_WORKING_DIR/cache/schema/toaster@2009-11-20.yang (toaster@2009-11-20.yang).

4. Read data from NETCONF device

Test the application through created REST endpoints:

  • start the Spring application
  • connect to NETCONF device with ID testDevice
curl -X PUT \
  http://localhost:8080/netconf/id/testDevice \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "admin",
    "password": "admin",
    "address": "127.0.0.1",
    "port": "17830"
}'
  • list all NETCONF devices
curl -X GET \
  http://localhost:8080/netconf/list \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \

if connection was successful the response will be

[
    {
        "nodeId": "testDevice",
        "connectionStatus": "Connected",
        "darknessFactor": 1000
    }
]

Notice that the darknessFactor was read from NETCONF device. Our testing device supports model toaster-model from ODL (link) so we were able to read the value defined in the model through MountPoint.

Michal Banik

Categories: lighty.io