Use NetconfNodeUtils.DEFAULT_TOPOLOGY_IID
[netconf.git] / apps / callhome-provider / src / main / java / org / opendaylight / netconf / callhome / mount / CallhomeStatusReporter.java
1 /*
2  * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.callhome.mount;
9
10 import com.google.common.util.concurrent.FutureCallback;
11 import com.google.common.util.concurrent.MoreExecutors;
12 import java.io.IOException;
13 import java.security.GeneralSecurityException;
14 import java.security.PublicKey;
15 import java.util.Collection;
16 import java.util.Collections;
17 import java.util.Optional;
18 import java.util.concurrent.ExecutionException;
19 import org.opendaylight.mdsal.binding.api.DataBroker;
20 import org.opendaylight.mdsal.binding.api.DataObjectModification;
21 import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
22 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
23 import org.opendaylight.mdsal.binding.api.DataTreeModification;
24 import org.opendaylight.mdsal.binding.api.ReadTransaction;
25 import org.opendaylight.mdsal.binding.api.WriteTransaction;
26 import org.opendaylight.mdsal.common.api.CommitInfo;
27 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
28 import org.opendaylight.netconf.callhome.protocol.AuthorizedKeysDecoder;
29 import org.opendaylight.netconf.callhome.protocol.StatusRecorder;
30 import org.opendaylight.netconf.topology.spi.NetconfNodeUtils;
31 import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1.DeviceStatus;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1Builder;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev221225.NetconfNode;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.NetconfCallhomeServer;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.AllowedDevices;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.Device;
38 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.DeviceBuilder;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.DeviceKey;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.device.transport.Ssh;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.device.transport.SshBuilder;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.device.transport.ssh.SshClientParamsBuilder;
43 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
44 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
45 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeKey;
46 import org.opendaylight.yangtools.concepts.ListenerRegistration;
47 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 final class CallhomeStatusReporter implements DataTreeChangeListener<Node>, StatusRecorder, AutoCloseable {
52     private static final Logger LOG = LoggerFactory.getLogger(CallhomeStatusReporter.class);
53
54     private final DataBroker dataBroker;
55     private final ListenerRegistration<CallhomeStatusReporter> reg;
56
57     CallhomeStatusReporter(final DataBroker broker) {
58         dataBroker = broker;
59         reg = dataBroker.registerDataTreeChangeListener(DataTreeIdentifier.create(LogicalDatastoreType.OPERATIONAL,
60             NetconfNodeUtils.DEFAULT_TOPOLOGY_IID.child(Node.class)), this);
61     }
62
63     @Override
64     public void onDataTreeChanged(final Collection<DataTreeModification<Node>> changes) {
65         for (DataTreeModification<Node> change : changes) {
66             final DataObjectModification<Node> rootNode = change.getRootNode();
67             final InstanceIdentifier<Node> identifier = change.getRootPath().getRootIdentifier();
68             switch (rootNode.getModificationType()) {
69                 case WRITE:
70                 case SUBTREE_MODIFIED:
71                     if (isNetconfNode(rootNode.getDataAfter())) {
72                         NodeId nodeId = getNodeId(identifier);
73                         if (nodeId != null) {
74                             NetconfNode nnode = rootNode.getDataAfter().augmentation(NetconfNode.class);
75                             handledNetconfNode(nodeId, nnode);
76                         }
77                     }
78                     break;
79                 case DELETE:
80                     if (isNetconfNode(rootNode.getDataBefore())) {
81                         final NodeId nodeId = getNodeId(identifier);
82                         if (nodeId != null) {
83                             handleDisconnectedNetconfNode(nodeId);
84                         }
85                     }
86                     break;
87                 default:
88                     break;
89             }
90         }
91     }
92
93     private static boolean isNetconfNode(final Node node) {
94         return node.augmentation(NetconfNode.class) != null;
95     }
96
97     private static NodeId getNodeId(final InstanceIdentifier<?> path) {
98         NodeKey key = path.firstKeyOf(Node.class);
99         return key != null ? key.getNodeId() : null;
100     }
101
102     private void handledNetconfNode(final NodeId nodeId, final NetconfNode nnode) {
103         switch (nnode.getConnectionStatus()) {
104             case Connected: {
105                 handleConnectedNetconfNode(nodeId);
106                 break;
107             }
108             default:
109             case UnableToConnect: {
110                 handleUnableToConnectNetconfNode(nodeId);
111                 break;
112             }
113         }
114     }
115
116     private void handleConnectedNetconfNode(final NodeId nodeId) {
117         // Fully connected, all services for remote device are
118         // available from the MountPointService.
119         LOG.debug("NETCONF Node: {} is fully connected", nodeId.getValue());
120
121         Device opDev = readAndGetDevice(nodeId);
122         if (opDev == null) {
123             LOG.warn("No corresponding callhome device found - exiting.");
124         } else {
125             Device modifiedDevice = withConnectedStatus(opDev);
126             if (modifiedDevice == null) {
127                 return;
128             }
129             LOG.info("Setting successful status for callhome device id:{}.", nodeId);
130             writeDevice(nodeId, modifiedDevice);
131         }
132     }
133
134     private void handleDisconnectedNetconfNode(final NodeId nodeId) {
135         LOG.debug("NETCONF Node: {} disconnected", nodeId.getValue());
136
137         Device opDev = readAndGetDevice(nodeId);
138         if (opDev == null) {
139             LOG.warn("No corresponding callhome device found - exiting.");
140         } else {
141             Device modifiedDevice = withDisconnectedStatus(opDev);
142             if (modifiedDevice == null) {
143                 return;
144             }
145             LOG.info("Setting disconnected status for callhome device id:{}.", nodeId);
146             writeDevice(nodeId, modifiedDevice);
147         }
148     }
149
150     private void handleUnableToConnectNetconfNode(final NodeId nodeId) {
151         // The maximum configured number of reconnect attempts
152         // have been reached. No more reconnects will be
153         // attempted by the Netconf Connector.
154         LOG.debug("NETCONF Node: {} connection failed", nodeId.getValue());
155
156         Device opDev = readAndGetDevice(nodeId);
157         if (opDev == null) {
158             LOG.warn("No corresponding callhome device found - exiting.");
159         } else {
160             Device modifiedDevice = withFailedStatus(opDev);
161             if (modifiedDevice == null) {
162                 return;
163             }
164             LOG.info("Setting failed status for callhome device id:{}.", nodeId);
165             writeDevice(nodeId, modifiedDevice);
166         }
167     }
168
169     void asForceListedDevice(final String id, final PublicKey serverKey) {
170         NodeId nid = new NodeId(id);
171         Device device = newDevice(id, serverKey, Device1.DeviceStatus.DISCONNECTED);
172         writeDevice(nid, device);
173     }
174
175     void asUnlistedDevice(final String id, final PublicKey serverKey) {
176         NodeId nid = new NodeId(id);
177         Device device = newDevice(id, serverKey, Device1.DeviceStatus.FAILEDNOTALLOWED);
178         writeDevice(nid, device);
179     }
180
181     private static Device newDevice(final String id, final PublicKey serverKey, final Device1.DeviceStatus status) {
182         // used only for netconf devices that are connected via SSH transport and global credentials
183         String sshEncodedKey = serverKey.toString();
184         try {
185             sshEncodedKey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
186         } catch (IOException e) {
187             LOG.warn("Unable to encode public key to ssh format.", e);
188         }
189         return new DeviceBuilder()
190             .setUniqueId(id)
191             .withKey(new DeviceKey(id))
192             .setTransport(new SshBuilder()
193                 .setSshClientParams(new SshClientParamsBuilder().setHostKey(sshEncodedKey).build())
194                 .build())
195             .addAugmentation(new Device1Builder().setDeviceStatus(status).build())
196             .build();
197     }
198
199     private Device readAndGetDevice(final NodeId nodeId) {
200         return readDevice(nodeId).orElse(null);
201     }
202
203     private Optional<Device> readDevice(final NodeId nodeId) {
204         try (ReadTransaction opTx = dataBroker.newReadOnlyTransaction()) {
205             InstanceIdentifier<Device> deviceIID = buildDeviceInstanceIdentifier(nodeId);
206             return opTx.read(LogicalDatastoreType.OPERATIONAL, deviceIID).get();
207         } catch (InterruptedException | ExecutionException e) {
208             return Optional.empty();
209         }
210     }
211
212     private void writeDevice(final NodeId nodeId, final Device modifiedDevice) {
213         WriteTransaction opTx = dataBroker.newWriteOnlyTransaction();
214         opTx.merge(LogicalDatastoreType.OPERATIONAL, buildDeviceInstanceIdentifier(nodeId), modifiedDevice);
215         commit(opTx, modifiedDevice.key());
216     }
217
218     private static InstanceIdentifier<Device> buildDeviceInstanceIdentifier(final NodeId nodeId) {
219         return InstanceIdentifier.create(NetconfCallhomeServer.class)
220             .child(AllowedDevices.class)
221             .child(Device.class, new DeviceKey(nodeId.getValue()));
222     }
223
224     private static Device withConnectedStatus(final Device opDev) {
225         return deviceWithStatus(opDev, Device1.DeviceStatus.CONNECTED);
226     }
227
228     private static Device withFailedStatus(final Device opDev) {
229         return deviceWithStatus(opDev, DeviceStatus.FAILED);
230     }
231
232     private static Device withDisconnectedStatus(final Device opDev) {
233         return deviceWithStatus(opDev, DeviceStatus.DISCONNECTED);
234     }
235
236     private static Device withFailedAuthStatus(final Device opDev) {
237         return deviceWithStatus(opDev, DeviceStatus.FAILEDAUTHFAILURE);
238     }
239
240     private static Device deviceWithStatus(final Device opDev, final DeviceStatus status) {
241         final DeviceBuilder deviceBuilder = new DeviceBuilder()
242             .setUniqueId(opDev.getUniqueId())
243             .addAugmentation(new Device1Builder().setDeviceStatus(status).build());
244         if (opDev.getTransport() != null) {
245             deviceBuilder.setTransport(opDev.getTransport());
246         } else {
247             deviceBuilder.setSshHostKey(opDev.getSshHostKey());
248         }
249         return deviceBuilder.build();
250     }
251
252     private void setDeviceStatus(final Device device) {
253         WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
254         InstanceIdentifier<Device> deviceIId = InstanceIdentifier.create(NetconfCallhomeServer.class)
255                         .child(AllowedDevices.class)
256                         .child(Device.class, device.key());
257
258         tx.merge(LogicalDatastoreType.OPERATIONAL, deviceIId, device);
259         commit(tx, device.key());
260     }
261
262     private static void commit(final WriteTransaction tx, final DeviceKey device) {
263         tx.commit().addCallback(new FutureCallback<CommitInfo>() {
264             @Override
265             public void onSuccess(final CommitInfo result) {
266                 LOG.debug("Device {} committed", device);
267             }
268
269             @Override
270             public void onFailure(final Throwable cause) {
271                 LOG.warn("Failed to commit device {}", device, cause);
272             }
273         }, MoreExecutors.directExecutor());
274     }
275
276     private AllowedDevices getDevices() {
277         try (ReadTransaction rxTransaction = dataBroker.newReadOnlyTransaction()) {
278             return rxTransaction.read(LogicalDatastoreType.OPERATIONAL, IetfZeroTouchCallHomeServerProvider.ALL_DEVICES)
279                     .get().orElse(null);
280         } catch (ExecutionException | InterruptedException e) {
281             LOG.error("Error trying to read the whitelist devices", e);
282             return null;
283         }
284     }
285
286     private Collection<Device> getDevicesAsList() {
287         AllowedDevices devices = getDevices();
288         return devices == null ? Collections.emptyList() : devices.nonnullDevice().values();
289     }
290
291     @Override
292     public void reportFailedAuth(final PublicKey sshKey) {
293         AuthorizedKeysDecoder decoder = new AuthorizedKeysDecoder();
294
295         for (final Device device : getDevicesAsList()) {
296             final String keyString;
297             if (device.getTransport() instanceof Ssh ssh) {
298                 keyString = ssh.getSshClientParams().getHostKey();
299             } else {
300                 keyString = device.getSshHostKey();
301             }
302             if (keyString == null) {
303                 LOG.info("Whitelist device {} does not have a host key, skipping it", device.getUniqueId());
304                 continue;
305             }
306
307             try {
308                 PublicKey pubKey = decoder.decodePublicKey(keyString);
309                 if (sshKey.getAlgorithm().equals(pubKey.getAlgorithm()) && sshKey.equals(pubKey)) {
310                     Device failedDevice = withFailedAuthStatus(device);
311                     if (failedDevice == null) {
312                         return;
313                     }
314                     LOG.info("Setting auth failed status for callhome device id:{}.", failedDevice.getUniqueId());
315                     setDeviceStatus(failedDevice);
316                     return;
317                 }
318             } catch (GeneralSecurityException e) {
319                 LOG.error("Failed decoding a device key with host key: {}", keyString, e);
320                 return;
321             }
322         }
323
324         LOG.error("No match found for the failed auth device (should have been filtered by whitelist). Key: {}",
325                 sshKey);
326     }
327
328     @Override
329     public void close() {
330         reg.close();
331     }
332 }