2 * Copyright (c) 2016 Brocade Communication Systems and others. All rights reserved.
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
8 package org.opendaylight.netconf.callhome.mount;
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;
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())));
62 private static final Logger LOG = LoggerFactory.getLogger(CallhomeStatusReporter.class);
64 private final DataBroker dataBroker;
65 private final ListenerRegistration<CallhomeStatusReporter> reg;
67 CallhomeStatusReporter(final DataBroker broker) {
69 reg = dataBroker.registerDataTreeChangeListener(DataTreeIdentifier.create(LogicalDatastoreType.OPERATIONAL,
70 NETCONF_TOPO_IID.child(Node.class)), this);
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()) {
80 case SUBTREE_MODIFIED:
81 if (isNetconfNode(rootNode.getDataAfter())) {
82 NodeId nodeId = getNodeId(identifier);
84 NetconfNode nnode = rootNode.getDataAfter().augmentation(NetconfNode.class);
85 handledNetconfNode(nodeId, nnode);
90 if (isNetconfNode(rootNode.getDataBefore())) {
91 final NodeId nodeId = getNodeId(identifier);
93 handleDisconnectedNetconfNode(nodeId);
103 private static boolean isNetconfNode(final Node node) {
104 return node.augmentation(NetconfNode.class) != null;
107 private static NodeId getNodeId(final InstanceIdentifier<?> path) {
108 NodeKey key = path.firstKeyOf(Node.class);
109 return key != null ? key.getNodeId() : null;
112 private void handledNetconfNode(final NodeId nodeId, final NetconfNode nnode) {
113 switch (nnode.getConnectionStatus()) {
115 handleConnectedNetconfNode(nodeId);
119 case UnableToConnect: {
120 handleUnableToConnectNetconfNode(nodeId);
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());
131 Device opDev = readAndGetDevice(nodeId);
133 LOG.warn("No corresponding callhome device found - exiting.");
135 Device modifiedDevice = withConnectedStatus(opDev);
136 if (modifiedDevice == null) {
139 LOG.info("Setting successful status for callhome device id:{}.", nodeId);
140 writeDevice(nodeId, modifiedDevice);
144 private void handleDisconnectedNetconfNode(final NodeId nodeId) {
145 LOG.debug("NETCONF Node: {} disconnected", nodeId.getValue());
147 Device opDev = readAndGetDevice(nodeId);
149 LOG.warn("No corresponding callhome device found - exiting.");
151 Device modifiedDevice = withDisconnectedStatus(opDev);
152 if (modifiedDevice == null) {
155 LOG.info("Setting disconnected status for callhome device id:{}.", nodeId);
156 writeDevice(nodeId, modifiedDevice);
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());
166 Device opDev = readAndGetDevice(nodeId);
168 LOG.warn("No corresponding callhome device found - exiting.");
170 Device modifiedDevice = withFailedStatus(opDev);
171 if (modifiedDevice == null) {
174 LOG.info("Setting failed status for callhome device id:{}.", nodeId);
175 writeDevice(nodeId, modifiedDevice);
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);
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);
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();
195 sshEncodedKey = AuthorizedKeysDecoder.encodePublicKey(serverKey);
196 } catch (IOException e) {
197 LOG.warn("Unable to encode public key to ssh format.", e);
199 final SshClientParams sshParams = new SshClientParamsBuilder().setHostKey(sshEncodedKey).build();
200 final Transport transport = new SshBuilder().setSshClientParams(sshParams).build();
201 return new DeviceBuilder()
203 .withKey(new DeviceKey(id))
204 .setTransport(transport)
205 .addAugmentation(new Device1Builder().setDeviceStatus(status).build())
209 private Device readAndGetDevice(final NodeId nodeId) {
210 return readDevice(nodeId).orElse(null);
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();
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());
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()));
234 private static Device withConnectedStatus(final Device opDev) {
235 return deviceWithStatus(opDev, Device1.DeviceStatus.CONNECTED);
238 private static Device withFailedStatus(final Device opDev) {
239 return deviceWithStatus(opDev, DeviceStatus.FAILED);
242 private static Device withDisconnectedStatus(final Device opDev) {
243 return deviceWithStatus(opDev, DeviceStatus.DISCONNECTED);
246 private static Device withFailedAuthStatus(final Device opDev) {
247 return deviceWithStatus(opDev, DeviceStatus.FAILEDAUTHFAILURE);
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());
257 deviceBuilder.setSshHostKey(opDev.getSshHostKey());
259 return deviceBuilder.build();
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());
268 tx.merge(LogicalDatastoreType.OPERATIONAL, deviceIId, device);
269 commit(tx, device.key());
272 private static void commit(final WriteTransaction tx, final DeviceKey device) {
273 tx.commit().addCallback(new FutureCallback<CommitInfo>() {
275 public void onSuccess(final CommitInfo result) {
276 LOG.debug("Device {} committed", device);
280 public void onFailure(final Throwable cause) {
281 LOG.warn("Failed to commit device {}", device, cause);
283 }, MoreExecutors.directExecutor());
286 private AllowedDevices getDevices() {
287 try (ReadTransaction rxTransaction = dataBroker.newReadOnlyTransaction()) {
288 return rxTransaction.read(LogicalDatastoreType.OPERATIONAL, IetfZeroTouchCallHomeServerProvider.ALL_DEVICES)
290 } catch (ExecutionException | InterruptedException e) {
291 LOG.error("Error trying to read the whitelist devices", e);
296 private Collection<Device> getDevicesAsList() {
297 AllowedDevices devices = getDevices();
298 return devices == null ? Collections.emptyList() : devices.nonnullDevice().values();
302 public void reportFailedAuth(final PublicKey sshKey) {
303 AuthorizedKeysDecoder decoder = new AuthorizedKeysDecoder();
305 for (final Device device : getDevicesAsList()) {
306 final String keyString;
307 if (device.getTransport() instanceof Ssh) {
308 keyString = ((Ssh) device.getTransport()).getSshClientParams().getHostKey();
310 keyString = device.getSshHostKey();
312 if (keyString == null) {
313 LOG.info("Whitelist device {} does not have a host key, skipping it", device.getUniqueId());
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) {
324 LOG.info("Setting auth failed status for callhome device id:{}.", failedDevice.getUniqueId());
325 setDeviceStatus(failedDevice);
328 } catch (GeneralSecurityException e) {
329 LOG.error("Failed decoding a device key with host key: {}", keyString, e);
334 LOG.error("No match found for the failed auth device (should have been filtered by whitelist). Key: {}",
339 public void close() {