Inline webSocketInitializer
[netconf.git] / docs / developer-guide.rst
1 .. _netconf-dev-guide:
2
3 NETCONF Developer Guide
4 =======================
5
6 .. note::
7
8     Reading the NETCONF section in the User Guide is likely useful as it
9     contains an overview of NETCONF in OpenDaylight and a how-to for
10     spawning and configuring NETCONF connectors.
11
12 This chapter is recommended for application developers who want to
13 interact with mounted NETCONF devices from their application code. It
14 tries to demonstrate all the use cases from user guide with RESTCONF but
15 now from the code level. One important difference would be the
16 demonstration of NETCONF notifications and notification listeners. The
17 notifications were not shown using RESTCONF because **RESTCONF does not
18 support notifications from mounted NETCONF devices.**
19
20 .. note::
21
22     It may also be useful to read the generic `OpenDaylight MD-SAL app
23     development
24     tutorial <https://wiki-archive.opendaylight.org/view/OpenDaylight_Controller:MD-SAL:MD-SAL_App_Tutorial>`__
25     before diving into this chapter. This guide assumes awareness of
26     basic OpenDaylight application development.
27
28 Sample app overview
29 -------------------
30
31 All the examples presented here are implemented by a sample OpenDaylight
32 application called **ncmount** in the ``coretutorials`` OpenDaylight
33 project. It can be found on the github mirror of OpenDaylight’s
34 repositories:
35
36 -  https://github.com/opendaylight/coretutorials/tree/master/ncmount
37
38 or checked out from the official OpenDaylight repository:
39
40 -  https://git.opendaylight.org/gerrit/admin/repos/coretutorials
41
42 **The application was built using the** `project startup maven
43 archetype <https://wiki-archive.opendaylight.org/view/OpenDaylight_Controller:MD-SAL:Startup_Project_Archetype>`__
44 **and demonstrates how to:**
45
46 -  preconfigure connectors to NETCONF devices
47
48 -  retrieve MountPointService (registry of available mount points)
49
50 -  listen and react to changing connection state of netconf-connector
51
52 -  add custom device YANG models to the app and work with them
53
54 -  read data from device in binding aware format (generated java APIs
55    from provided YANG models)
56
57 -  write data into device in binding aware format
58
59 -  trigger and listen to NETCONF notifications in binding aware format
60
61 Detailed information about the structure of the application can be found
62 at:
63 https://wiki-archive.opendaylight.org/view/Controller_Core_Functionality_Tutorials:Tutorials:Netconf_Mount
64
65 .. note::
66
67     The code in ncmount is fully **binding aware** (works with generated
68     java APIs from provided YANG models). However it is also possible to
69     perform the same operations in **binding independent** manner.
70
71 NcmountProvider
72 ~~~~~~~~~~~~~~~
73
74 The NcmountProvider class (found in NcmountProvider.java) is the central
75 point of the ncmount application and all the application logic is
76 contained there. The following sections will detail its most interesting
77 pieces.
78
79 Retrieve MountPointService
80 ^^^^^^^^^^^^^^^^^^^^^^^^^^
81
82 The MountPointService is a central registry of all available mount
83 points in OpenDaylight. It is just another MD-SAL service and is
84 available from the ``session`` attribute passed by
85 ``onSessionInitiated`` callback:
86
87 ::
88
89     @Override
90     public void onSessionInitiated(ProviderContext session) {
91         LOG.info("NcmountProvider Session Initiated");
92
93         // Get references to the data broker and mount service
94         this.mountService = session.getSALService(MountPointService.class);
95
96         ...
97
98         }
99     }
100
101 Listen for connection state changes
102 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
103
104 It is important to know when a mount point appears, when it is fully
105 connected and when it is disconnected or removed. The exact states of a
106 mount point are:
107
108 -  Connected
109
110 -  Connecting
111
112 -  Unable to connect
113
114 To receive this kind of information, an application has to register
115 itself as a notification listener for the preconfigured netconf-topology
116 subtree in MD-SAL’s datastore. This can be performed in the
117 ``onSessionInitiated`` callback as well:
118
119 ::
120
121     @Override
122     public void onSessionInitiated(ProviderContext session) {
123
124         ...
125
126         this.dataBroker = session.getSALService(DataBroker.class);
127
128         // Register ourselves as the REST API RPC implementation
129         this.rpcReg = session.addRpcImplementation(NcmountService.class, this);
130
131         // Register ourselves as data change listener for changes on Netconf
132         // nodes. Netconf nodes are accessed via "Netconf Topology" - a special
133         // topology that is created by the system infrastructure. It contains
134         // all Netconf nodes the Netconf connector knows about. NETCONF_TOPO_IID
135         // is equivalent to the following URL:
136         // .../restconf/operational/network-topology:network-topology/topology/topology-netconf
137         if (dataBroker != null) {
138             this.dclReg = dataBroker.registerDataChangeListener(LogicalDatastoreType.OPERATIONAL,
139                     NETCONF_TOPO_IID.child(Node.class),
140                     this,
141                     DataChangeScope.SUBTREE);
142         }
143     }
144
145 The implementation of the callback from MD-SAL when the data change can
146 be found in the
147 ``onDataChanged(AsyncDataChangeEvent<InstanceIdentifier<?>, DataObject>
148 change)`` callback of `NcmountProvider
149 class <https://github.com/opendaylight/coretutorials/blob/master/ncmount/impl/src/main/java/ncmount/impl/NcmountProvider.java>`__.
150
151 Reading data from the device
152 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
153
154 The first step when trying to interact with the device is to get the
155 exact mount point instance (identified by an instance identifier) from
156 the MountPointService:
157
158 ::
159
160     @Override
161     public Future<RpcResult<ShowNodeOutput>> showNode(ShowNodeInput input) {
162         LOG.info("showNode called, input {}", input);
163
164         // Get the mount point for the specified node
165         // Equivalent to '.../restconf/<config | operational>/opendaylight-inventory:nodes/node/<node-name>/yang-ext:mount/'
166         // Note that we can read both config and operational data from the same
167         // mount point
168         final Optional<MountPoint> xrNodeOptional = mountService.getMountPoint(NETCONF_TOPO_IID
169                 .child(Node.class, new NodeKey(new NodeId(input.getNodeName()))));
170
171         Preconditions.checkArgument(xrNodeOptional.isPresent(),
172                 "Unable to locate mountpoint: %s, not mounted yet or not configured",
173                 input.getNodeName());
174         final MountPoint xrNode = xrNodeOptional.get();
175
176         ....
177     }
178
179 .. note::
180
181     The triggering method in this case is called ``showNode``. It is a
182     YANG-defined RPC and NcmountProvider serves as an MD-SAL RPC
183     implementation among other things. This means that ``showNode`` an
184     be triggered using RESTCONF.
185
186 The next step is to retrieve an instance of the ``DataBroker`` API from
187 the mount point and start a read transaction:
188
189 ::
190
191     @Override
192     public Future<RpcResult<ShowNodeOutput>> showNode(ShowNodeInput input) {
193
194         ...
195
196         // Get the DataBroker for the mounted node
197         final DataBroker xrNodeBroker = xrNode.getService(DataBroker.class).get();
198         // Start a new read only transaction that we will use to read data
199         // from the device
200         final ReadOnlyTransaction xrNodeReadTx = xrNodeBroker.newReadOnlyTransaction();
201
202         ...
203     }
204
205 Finally, it is possible to perform the read operation:
206
207 ::
208
209     @Override
210     public Future<RpcResult<ShowNodeOutput>> showNode(ShowNodeInput input) {
211
212         ...
213
214         InstanceIdentifier<InterfaceConfigurations> iid =
215                 InstanceIdentifier.create(InterfaceConfigurations.class);
216
217         Optional<InterfaceConfigurations> ifConfig;
218         try {
219             // Read from a transaction is asynchronous, but a simple
220             // get/checkedGet makes the call synchronous
221             ifConfig = xrNodeReadTx.read(LogicalDatastoreType.CONFIGURATION, iid).checkedGet();
222         } catch (ReadFailedException e) {
223             throw new IllegalStateException("Unexpected error reading data from " + input.getNodeName(), e);
224         }
225
226         ...
227     }
228
229 The instance identifier is used here again to specify a subtree to read
230 from the device. At this point application can process the data as it
231 sees fit. The ncmount app transforms the data into its own format and
232 returns it from ``showNode``.
233
234 .. note::
235
236     More information can be found in the source code of ncmount sample
237     app + on wiki:
238     https://wiki-archive.opendaylight.org/view/Controller_Core_Functionality_Tutorials:Tutorials:Netconf_Mount
239
240 Reading selected fields from device
241 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
242
243 Using NETCONF DOM API it is also possible to read only selected fields
244 from NETCONF device. NETCONF mountpoint exposes 2 services that provides
245 this functionality:
246
247 a. **NetconfDataTreeService** - It provides overloaded methods 'get'
248    (operational data) and 'getConfig' (configuration data)
249    with 'List<YangInstanceIdentifier> fields' parameters. This service
250    should be used if transaction properties are not required.
251 b. **NetconfDOMDataBrokerFieldsExtension** - It implements DOMDataBroker
252    interface - provides the same transaction functionality plus method
253    for reading of data with selected fields: read(LogicalDatastoreType,
254    YangInstanceIdentifier, List<YangInstanceIdentifier>).
255    Instance of NetconfDOMDataBrokerFieldsExtension can be obtained
256    using 'getExtensions()' method on the DOMDataBroker instance.
257
258 'List<YangInstanceIdentifier> fields' parameter specifies list of paths
259 that are read from device using subtree filtering. These paths must be
260 relative to parent path - returned data follows schema of the last path
261 argument of parent path.
262
263 **Mechanics**
264
265 * parent path: /a/b/c
266 * fields: [/x/y; /w/z]
267
268 NETCONF server will read data on following paths:
269 * /a/b/c/x/y
270 * /a/b/c/w/z
271
272 And place read data under /a/b/c path - thus returned data will have
273 following structure:
274
275 .. code-block::
276
277     c: {
278         x: {
279             y: {
280                 // data ...
281             }
282         }
283         w: {
284             z: {
285                 // data ...
286             }
287         }
288     }
289
290 From the view of DOM API, YangInstanceIdentifier (first parameter) represents
291 the parent path, and List<YangInstanceIdentifier> represents list of selected
292 fields.
293
294 **Example: using NetconfDataTreeService**
295
296 The following method demonstrates reading of 2 fields under parent list 'l1':
297
298 * parent path: /c1/l1
299 * fields (leaves 'x1' and 'x2'): [/c2/x1, /c2/l2/x2]
300
301 Result will contain whole MapNode with possibly multiple MapEntryNode-s that
302 contain only keys of list elements and selected fields.
303
304 .. code-block:: java
305
306     public void readData(final DOMMountPoint mountPoint) {
307         final NetconfDataTreeService dataTreeService = mountPoint.getService(NetconfDataTreeService.class).get();
308
309         final YangInstanceIdentifier parentPath = YangInstanceIdentifier.builder()
310                 .node(CONTAINER_C1_NI)   // container 'c1' (root element)
311                 .node(LIST_L1_NI)        // list 'l1' placed under container 'c1'
312                 .build();
313         final YangInstanceIdentifier leafX1Field = YangInstanceIdentifier.builder()
314                 .node(CONTAINER_C2_NI)   // container 'c2' placed under list 'l1'
315                 .node(LEAF_X1_NI)        // leaf 'x1' placed under container 'c2'
316                 .build();
317         final YangInstanceIdentifier leafX2Field = YangInstanceIdentifier.builder()
318                 .node(CONTAINER_C2_NI)   // container 'c2' placed under list 'l1'
319                 .node(NESTED_LIST_L2_NI) // list 'l2' placed under container 'c2'
320                 .node(LEAF_X2_NI)        // leaf 'x2' placed under list 'l2'
321                 .build();
322
323         final ListenableFuture<Optional<NormalizedNode<?, ?>>> config = dataTreeService.getConfig(
324                 parentPath, Lists.newArrayList(leafX1Field, leafX2Field));
325
326         Futures.addCallback(config, new FutureCallback<Optional<NormalizedNode<?, ?>>>() {
327             @Override
328             public void onSuccess(@Nullable final Optional<NormalizedNode<?, ?>> normalizedNode) {
329                 normalizedNode.ifPresent(node -> LOG.info("Read data: {}", NormalizedNodes.toStringTree(node)));
330                 /*
331                 We expect data with following structure:
332                 l1: [
333                     {
334                         key-l1: "k1",
335                         c2: {
336                             x1: 10,
337                             l2: [
338                                 {
339                                     key-l2: 1,
340                                     x2: "foo"
341                                 },
342                                 ...
343                             ]
344                         }
345                     },
346                     ...
347                 ]
348                 */
349             }
350
351             @Override
352             public void onFailure(final Throwable throwable) {
353                 LOG.error("Failed to read data: {}", parentPath, throwable);
354             }
355         });
356     }
357
358 **Example: using NetconfDOMDataBrokerFieldsExtension**
359
360 The following method demonstrates reading of 2 fields under parent
361 container 'c1':
362
363 * parent path: /c1
364 * fields (leaf-list 'll1 'and container 'c2'): [/ll1, /l1=k1/c2]
365
366 Result will contain ContainerNode with identifier 'c1' and children
367 nodes (if they are present) of type LeafSetNode and MapNode.
368
369 .. code-block:: java
370
371     public void readData(final DOMMountPoint mountPoint) {
372         final DOMDataBroker domDataBroker = mountPoint.getService(DOMDataBroker.class).get();
373         final NetconfDOMDataBrokerFieldsExtension domFieldsDataBroker = domDataBroker.getExtensions().getInstance(
374                 NetconfDOMDataBrokerFieldsExtension.class);
375
376         final YangInstanceIdentifier parentPath = YangInstanceIdentifier.builder()
377                 .node(CONTAINER_C1_NI)   // container 'c1' (root element)
378                 .build();
379         final YangInstanceIdentifier ll1Field = YangInstanceIdentifier.builder()
380                 .node(LEAF_LIST_LL1_NI)  // leaf-list 'll1' placed under container 'c1'
381                 .build();
382         final YangInstanceIdentifier c2Field = YangInstanceIdentifier.builder()
383                 .node(LIST_L1_NI)        // list 'l1' placed under container 'c1'
384                 .nodeWithKey(LIST_L1_QN, LIST_L1_KEY_QN, "k1") // specific list entry with key value 'k1'
385                 .node(CONTAINER_C2_NI)   // container 'c2' placed under container 'c1'
386                 .build();
387
388         final FluentFuture<Optional<NormalizedNode<?, ?>>> data;
389         try (NetconfDOMFieldsReadTransaction roTx = domFieldsDataBroker.newReadOnlyTransaction()) {
390            data = roTx.read(LogicalDatastoreType.CONFIGURATION, parentPath, Lists.newArrayList(ll1Field, c2Field));
391         }
392
393         data.addCallback(new FutureCallback<>() {
394             @Override
395             public void onSuccess(@Nullable final Optional<NormalizedNode<?, ?>> normalizedNode) {
396                 normalizedNode.ifPresent(node -> LOG.info("Read data: {}", NormalizedNodes.toStringTree(node)));
397                 /*
398                 We expect data with following structure:
399                 c1: {
400                     ll1: [...],
401                     l1: [
402                         {
403                             l1-key: "k1",
404                             c2: {
405                                 // data ...
406                             }
407                         }
408                     ]
409                 }
410                 */
411             }
412
413             @Override
414             public void onFailure(final Throwable throwable) {
415                 LOG.error("Failed to read data: {}", parentPath, throwable);
416             }
417         }, MoreExecutors.directExecutor());
418     }