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