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 a 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 an OpenDaylight based NETCONF SDN controller application in the spring-boot environment.
The architecture of this example is in the picture.
Generate Spring Boot Skeleton Project
First, generate the 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
description: Demo lighty.io project for Spring Boot
package: io.lighty.core.controller.springboot
packaging: jar
java version: 8
Start.spring.io generates for us a basic maven project hierarchy with an application entry point in class LightyControllerSpringbootApplication.
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 a bean provider for Spring’s @Configuration.
8.0.0-SNAPSHOT io.lighty.core lighty-controller-spring-di ${lighty.version}
And to use the default single node lighty.io configuration add a dependency
io.lighty.resources singlenode-configuration ${lighty.version}
For the NETCONF devices, we will need also the NETCONF Southbound module
io.lighty.modules.southbound.netconf lighty-netconf-sb ${lighty.version}
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 necessary to create lighty.io core controller as Spring bean, for example in class LightyConfiguration. In this example, we need to initialize LightyController and NETCONF Southbound Plugin 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 SetmavenModelPaths = 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 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 the Spring environment. Let’s add some basic logic.
Create a Basic lighty.io REST Service
In this step, we will create a simple REST service, exposing endpoints for an example of managing NETCONF devices.
Let’s add endpoints which provide us functionality to:
- connect to a device
- list all devices
- disconnect device
Simply create a @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 the request body
- DELETE /netconf/id/{netconfDeviceId} will disconnect device and removed it from datastore
All 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 OpenDaylight’s global data store.
@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 InstanceIdentifierNETCONF_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 netconfTopoOptional = tx.read(LogicalDatastoreType.OPERATIONAL, NETCONF_TOPOLOGY_IID).get(TIMEOUT, TimeUnit.SECONDS); if (netconfTopoOptional.isPresent() && netconfTopoOptional.get().getNode() != null) { final List response = new ArrayList<>(); for (Node node : netconfTopoOptional.get().getNode()) { final NetconfDeviceResponse nodeResponse = NetconfDeviceResponse.from(node); response.add(nodeResponse); final Optional netconfMountPoint = mountPointService.getMountPoint( NETCONF_TOPOLOGY_IID.child(Node.class, new NodeKey(node.getNodeId())) ); if (netconfMountPoint.isPresent()) { final Optional netconfDataBroker = netconfMountPoint.get().getService(DataBroker.class); if (netconfDataBroker.isPresent()) { final ReadOnlyTransaction netconfReadTx = netconfDataBroker.get().newReadOnlyTransaction(); final Optional 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 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 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 the device yang model to PROJECT_WORKING_DIR/cache/schema/toaster@2009-11-20.yang (toaster@2009-11-20.yang).
Read Data from the 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 the connection was successful, the response will be
[ { "nodeId": "testDevice", "connectionStatus": "Connected", "darknessFactor": 1000 } ]
Notice that the darknessFactor was read from the NETCONF device. Our testing device supports model toaster-model from OpenDaylight so we were able to read the value defined in the model through MountPoint.
Michal Baník