Avoid returning null Optional
[netconf.git] / netconf / 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
9 package org.opendaylight.netconf.callhome.mount;
10
11 import com.google.common.base.Optional;
12 import com.google.common.util.concurrent.CheckedFuture;
13 import java.io.IOException;
14 import java.security.NoSuchAlgorithmException;
15 import java.security.NoSuchProviderException;
16 import java.security.PublicKey;
17 import java.security.spec.InvalidKeySpecException;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Map;
21 import javax.annotation.Nonnull;
22 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
23 import org.opendaylight.controller.md.sal.binding.api.DataChangeListener;
24 import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
25 import org.opendaylight.controller.md.sal.binding.api.ReadWriteTransaction;
26 import org.opendaylight.controller.md.sal.binding.api.WriteTransaction;
27 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker;
28 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataChangeEvent;
29 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
30 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
31 import org.opendaylight.netconf.callhome.protocol.AuthorizedKeysDecoder;
32 import org.opendaylight.netconf.callhome.protocol.StatusRecorder;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1;
34 import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1Builder;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNodeConnectionStatus;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.network.topology.topology.topology.types.TopologyNetconf;
38 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.NetconfCallhomeServer;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.AllowedDevices;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceBuilder;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceKey;
43 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
44 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
45 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TopologyId;
46 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.Topology;
47 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.TopologyKey;
48 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
49 import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeKey;
50 import org.opendaylight.yangtools.concepts.ListenerRegistration;
51 import org.opendaylight.yangtools.yang.binding.DataObject;
52 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 class CallhomeStatusReporter implements DataChangeListener, StatusRecorder, AutoCloseable {
57     private static final InstanceIdentifier<Topology> NETCONF_TOPO_IID =
58             InstanceIdentifier.create(NetworkTopology.class).child(Topology.class,
59                     new TopologyKey(new TopologyId(TopologyNetconf.QNAME.getLocalName())));
60
61     private static final Logger LOG = LoggerFactory.getLogger(CallhomeStatusReporter.class);
62
63     private final DataBroker dataBroker;
64     private final ListenerRegistration<DataChangeListener> reg;
65
66     CallhomeStatusReporter(DataBroker broker) {
67         this.dataBroker = broker;
68         this.reg = dataBroker.registerDataChangeListener(LogicalDatastoreType.OPERATIONAL,
69             NETCONF_TOPO_IID.child(Node.class), this, AsyncDataBroker.DataChangeScope.SUBTREE);
70     }
71
72     @Override
73     public void onDataChanged(AsyncDataChangeEvent<InstanceIdentifier<?>, DataObject> change) {
74         for (InstanceIdentifier<?> removedPath : change.getRemovedPaths()) {
75             if (removedPath.getTargetType() != NetconfNode.class) {
76                 continue;
77             }
78
79             final NodeId nodeId = getNodeId(removedPath);
80             if (nodeId != null) {
81                 handleDisconnectedNetconfNode(nodeId);
82                 return;
83             }
84         }
85
86         for (Map.Entry<InstanceIdentifier<?>, DataObject> entry : change.getUpdatedData().entrySet()) {
87             if (entry.getKey().getTargetType() == NetconfNode.class) {
88                 NodeId nodeId = getNodeId(entry.getKey());
89                 if (nodeId != null) {
90                     NetconfNode nnode = (NetconfNode) entry.getValue();
91                     handledNetconfNode(nodeId, nnode);
92                     return;
93                 }
94             }
95         }
96     }
97
98     private NodeId getNodeId(final InstanceIdentifier<?> path) {
99         NodeKey key = path.firstKeyOf(Node.class);
100         return key != null ? key.getNodeId() : null;
101     }
102
103     private void handledNetconfNode(NodeId nodeId, NetconfNode nnode) {
104         NetconfNodeConnectionStatus.ConnectionStatus csts = nnode.getConnectionStatus();
105
106         switch (csts) {
107             case Connected: {
108                 handleConnectedNetconfNode(nodeId);
109                 break;
110             }
111             default:
112             case UnableToConnect: {
113                 handleUnableToConnectNetconfNode(nodeId);
114                 break;
115             }
116         }
117     }
118
119     private void handleConnectedNetconfNode(NodeId nodeId) {
120         // Fully connected, all services for remote device are
121         // available from the MountPointService.
122         LOG.debug("NETCONF Node: {} is fully connected", nodeId.getValue());
123
124         Device opDev = readAndGetDevice(nodeId);
125         if (opDev == null) {
126             LOG.warn("No corresponding callhome device found - exiting.");
127         } else {
128             Device modifiedDevice = withConnectedStatus(opDev);
129             if (modifiedDevice == null) {
130                 return;
131             }
132             LOG.info("Setting successful status for callhome device id:{}.", nodeId);
133             writeDevice(nodeId, modifiedDevice);
134         }
135     }
136
137     private void handleDisconnectedNetconfNode(NodeId nodeId) {
138         LOG.debug("NETCONF Node: {} disconnected", nodeId.getValue());
139
140         Device opDev = readAndGetDevice(nodeId);
141         if (opDev == null) {
142             LOG.warn("No corresponding callhome device found - exiting.");
143         } else {
144             Device modifiedDevice = withDisconnectedStatus(opDev);
145             if (modifiedDevice == null) {
146                 return;
147             }
148             LOG.info("Setting disconnected status for callhome device id:{}.", nodeId);
149             writeDevice(nodeId, modifiedDevice);
150         }
151     }
152
153     private void handleUnableToConnectNetconfNode(NodeId nodeId) {
154         // The maximum configured number of reconnect attempts
155         // have been reached. No more reconnects will be
156         // attempted by the Netconf Connector.
157         LOG.debug("NETCONF Node: {} connection failed", nodeId.getValue());
158
159         Device opDev = readAndGetDevice(nodeId);
160         if (opDev == null) {
161             LOG.warn("No corresponding callhome device found - exiting.");
162         } else {
163             Device modifiedDevice = withFailedStatus(opDev);
164             if (modifiedDevice == null) {
165                 return;
166             }
167             LOG.info("Setting failed status for callhome device id:{}.", nodeId);
168             writeDevice(nodeId, modifiedDevice);
169         }
170     }
171
172     void asForceListedDevice(String id, PublicKey serverKey) {
173         NodeId nid = new NodeId(id);
174         Device device = newDevice(id, serverKey, Device1.DeviceStatus.DISCONNECTED);
175         writeDevice(nid, device);
176     }
177
178     void asUnlistedDevice(String id, PublicKey serverKey) {
179         NodeId nid = new NodeId(id);
180         Device device = newDevice(id, serverKey, Device1.DeviceStatus.FAILEDNOTALLOWED);
181         writeDevice(nid, device);
182     }
183
184     private Device newDevice(String id, PublicKey serverKey, Device1.DeviceStatus status) {
185         String sshEncodedKey = serverKey.toString();
186         try {
187             sshEncodedKey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
188         } catch (IOException e) {
189             LOG.warn("Unable to encode public key to ssh format.", e);
190         }
191         Device1 d1 = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.FAILEDNOTALLOWED).build();
192         DeviceBuilder builder = new DeviceBuilder()
193                 .setUniqueId(id)
194                 .setKey(new DeviceKey(id))
195                 .setSshHostKey(sshEncodedKey)
196                 .addAugmentation(Device1.class, d1);
197
198         return builder.build();
199     }
200
201     private Device readAndGetDevice(NodeId nodeId) {
202         return readDevice(nodeId).orNull();
203     }
204
205     @Nonnull
206     private Optional<Device> readDevice(NodeId nodeId) {
207         ReadOnlyTransaction opTx = dataBroker.newReadOnlyTransaction();
208
209         InstanceIdentifier<Device> deviceIID = buildDeviceInstanceIdentifier(nodeId);
210         CheckedFuture<Optional<Device>, ReadFailedException> devFuture =
211                 opTx.read(LogicalDatastoreType.OPERATIONAL, deviceIID);
212
213         try {
214             return devFuture.checkedGet();
215         } catch (ReadFailedException e) {
216             return Optional.absent();
217         }
218     }
219
220     private void writeDevice(NodeId nodeId, Device modifiedDevice) {
221         ReadWriteTransaction opTx = dataBroker.newReadWriteTransaction();
222         opTx.merge(LogicalDatastoreType.OPERATIONAL, buildDeviceInstanceIdentifier(nodeId), modifiedDevice);
223         opTx.submit();
224     }
225
226     private InstanceIdentifier<Device> buildDeviceInstanceIdentifier(NodeId nodeId) {
227         return InstanceIdentifier.create(NetconfCallhomeServer.class)
228                 .child(AllowedDevices.class)
229                 .child(Device.class, new DeviceKey(nodeId.getValue()));
230     }
231
232     private Device withConnectedStatus(Device opDev) {
233         Device1 status = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.CONNECTED).build();
234         return new DeviceBuilder().addAugmentation(Device1.class, status).setUniqueId(opDev.getUniqueId())
235                 .setSshHostKey(opDev.getSshHostKey()).build();
236     }
237
238     private Device withFailedStatus(Device opDev) {
239         Device1 status = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.FAILED).build();
240         return new DeviceBuilder().addAugmentation(Device1.class, status).setUniqueId(opDev.getUniqueId())
241                 .setSshHostKey(opDev.getSshHostKey()).build();
242     }
243
244     private Device withDisconnectedStatus(Device opDev) {
245         Device1 status = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.DISCONNECTED).build();
246         return new DeviceBuilder().addAugmentation(Device1.class, status).setUniqueId(opDev.getUniqueId())
247                 .setSshHostKey(opDev.getSshHostKey()).build();
248     }
249
250     private Device withFailedAuthStatus(Device opDev) {
251         Device1 status = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.FAILEDAUTHFAILURE).build();
252         return new DeviceBuilder().addAugmentation(Device1.class, status).setUniqueId(opDev.getUniqueId())
253                 .setSshHostKey(opDev.getSshHostKey()).build();
254     }
255
256     private void setDeviceStatus(Device device) {
257         WriteTransaction tx = dataBroker.newWriteOnlyTransaction();
258         InstanceIdentifier<Device> deviceIId =
259                 InstanceIdentifier.create(NetconfCallhomeServer.class)
260                         .child(AllowedDevices.class)
261                         .child(Device.class, device.getKey());
262
263         tx.merge(LogicalDatastoreType.OPERATIONAL, deviceIId, device);
264         tx.submit();
265     }
266
267     private AllowedDevices getDevices() {
268         ReadOnlyTransaction rxTransaction = dataBroker.newReadOnlyTransaction();
269         CheckedFuture<Optional<AllowedDevices>, ReadFailedException> devicesFuture =
270                 rxTransaction.read(LogicalDatastoreType.OPERATIONAL, IetfZeroTouchCallHomeServerProvider.ALL_DEVICES);
271
272         try {
273             Optional<AllowedDevices> opt = devicesFuture.checkedGet();
274             if (opt.isPresent()) {
275                 AllowedDevices devices = opt.get();
276                 return devices;
277             }
278         } catch (ReadFailedException e) {
279             LOG.error("Error trying to read the whitelist devices: {}", e);
280         }
281
282         return null;
283     }
284
285     private List<Device> getDevicesAsList() {
286         AllowedDevices devices = getDevices();
287         return (devices == null) ? new ArrayList<Device>() : devices.getDevice();
288     }
289
290     @Override
291     public void reportFailedAuth(PublicKey sshKey) {
292         AuthorizedKeysDecoder decoder = new AuthorizedKeysDecoder();
293
294         for (Device device : getDevicesAsList()) {
295             String keyString = device.getSshHostKey();
296
297             try {
298                 PublicKey pubKey = decoder.decodePublicKey(keyString);
299                 if (sshKey.getAlgorithm().equals(pubKey.getAlgorithm()) && sshKey.equals(pubKey)) {
300                     Device failedDevice = withFailedAuthStatus(device);
301                     if (failedDevice == null) {
302                         return;
303                     }
304                     LOG.info("Setting auth failed status for callhome device id:{}.", failedDevice.getUniqueId());
305                     setDeviceStatus(failedDevice);
306                     return;
307                 }
308             } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchProviderException e) {
309                 LOG.error("Failed decoding a device key with host key: {} {}", keyString, e);
310                 return;
311             }
312         }
313
314         LOG.error("No match found for the failed auth device (should have been filtered by whitelist). Key: {}",
315                 sshKey);
316     }
317
318     @Override
319     public void close() throws Exception {
320         reg.close();
321     }
322 }