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