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