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