/* * Copyright (c) 2016 Brocade Communication Systems and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.netconf.callhome.mount; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.PublicKey; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import org.opendaylight.mdsal.binding.api.DataBroker; import org.opendaylight.mdsal.binding.api.DataObjectModification; import org.opendaylight.mdsal.binding.api.DataTreeChangeListener; import org.opendaylight.mdsal.binding.api.DataTreeIdentifier; import org.opendaylight.mdsal.binding.api.DataTreeModification; import org.opendaylight.mdsal.binding.api.ReadTransaction; import org.opendaylight.mdsal.binding.api.WriteTransaction; import org.opendaylight.mdsal.common.api.CommitInfo; import org.opendaylight.mdsal.common.api.LogicalDatastoreType; import org.opendaylight.netconf.callhome.protocol.AuthorizedKeysDecoder; import org.opendaylight.netconf.callhome.protocol.StatusRecorder; import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1; import org.opendaylight.yang.gen.v1.urn.opendaylight.callhome.device.status.rev170112.Device1Builder; import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode; import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNodeConnectionStatus; import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.network.topology.topology.topology.types.TopologyNetconf; import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.NetconfCallhomeServer; import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.AllowedDevices; import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device; import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceBuilder; import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.DeviceKey; import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology; import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId; import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TopologyId; import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.Topology; import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.TopologyKey; import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node; import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeKey; import org.opendaylight.yangtools.concepts.ListenerRegistration; import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; class CallhomeStatusReporter implements DataTreeChangeListener, StatusRecorder, AutoCloseable { private static final InstanceIdentifier NETCONF_TOPO_IID = InstanceIdentifier.create(NetworkTopology.class).child(Topology.class, new TopologyKey(new TopologyId(TopologyNetconf.QNAME.getLocalName()))); private static final Logger LOG = LoggerFactory.getLogger(CallhomeStatusReporter.class); private final DataBroker dataBroker; private final ListenerRegistration reg; CallhomeStatusReporter(final DataBroker broker) { this.dataBroker = broker; this.reg = dataBroker.registerDataTreeChangeListener(DataTreeIdentifier.create(LogicalDatastoreType.OPERATIONAL, NETCONF_TOPO_IID.child(Node.class)), this); } @Override public void onDataTreeChanged(final Collection> changes) { for (DataTreeModification change: changes) { final DataObjectModification rootNode = change.getRootNode(); final InstanceIdentifier identifier = change.getRootPath().getRootIdentifier(); switch (rootNode.getModificationType()) { case WRITE: case SUBTREE_MODIFIED: if (isNetconfNode(rootNode.getDataAfter())) { NodeId nodeId = getNodeId(identifier); if (nodeId != null) { NetconfNode nnode = rootNode.getDataAfter().augmentation(NetconfNode.class); handledNetconfNode(nodeId, nnode); } } break; case DELETE: if (isNetconfNode(rootNode.getDataBefore())) { final NodeId nodeId = getNodeId(identifier); if (nodeId != null) { handleDisconnectedNetconfNode(nodeId); } } break; default: break; } } } private static boolean isNetconfNode(final Node node) { return node.augmentation(NetconfNode.class) != null; } private static NodeId getNodeId(final InstanceIdentifier path) { NodeKey key = path.firstKeyOf(Node.class); return key != null ? key.getNodeId() : null; } private void handledNetconfNode(final NodeId nodeId, final NetconfNode nnode) { NetconfNodeConnectionStatus.ConnectionStatus csts = nnode.getConnectionStatus(); switch (csts) { case Connected: { handleConnectedNetconfNode(nodeId); break; } default: case UnableToConnect: { handleUnableToConnectNetconfNode(nodeId); break; } } } private void handleConnectedNetconfNode(final NodeId nodeId) { // Fully connected, all services for remote device are // available from the MountPointService. LOG.debug("NETCONF Node: {} is fully connected", nodeId.getValue()); Device opDev = readAndGetDevice(nodeId); if (opDev == null) { LOG.warn("No corresponding callhome device found - exiting."); } else { Device modifiedDevice = withConnectedStatus(opDev); if (modifiedDevice == null) { return; } LOG.info("Setting successful status for callhome device id:{}.", nodeId); writeDevice(nodeId, modifiedDevice); } } private void handleDisconnectedNetconfNode(final NodeId nodeId) { LOG.debug("NETCONF Node: {} disconnected", nodeId.getValue()); Device opDev = readAndGetDevice(nodeId); if (opDev == null) { LOG.warn("No corresponding callhome device found - exiting."); } else { Device modifiedDevice = withDisconnectedStatus(opDev); if (modifiedDevice == null) { return; } LOG.info("Setting disconnected status for callhome device id:{}.", nodeId); writeDevice(nodeId, modifiedDevice); } } private void handleUnableToConnectNetconfNode(final NodeId nodeId) { // The maximum configured number of reconnect attempts // have been reached. No more reconnects will be // attempted by the Netconf Connector. LOG.debug("NETCONF Node: {} connection failed", nodeId.getValue()); Device opDev = readAndGetDevice(nodeId); if (opDev == null) { LOG.warn("No corresponding callhome device found - exiting."); } else { Device modifiedDevice = withFailedStatus(opDev); if (modifiedDevice == null) { return; } LOG.info("Setting failed status for callhome device id:{}.", nodeId); writeDevice(nodeId, modifiedDevice); } } void asForceListedDevice(final String id, final PublicKey serverKey) { NodeId nid = new NodeId(id); Device device = newDevice(id, serverKey, Device1.DeviceStatus.DISCONNECTED); writeDevice(nid, device); } void asUnlistedDevice(final String id, final PublicKey serverKey) { NodeId nid = new NodeId(id); Device device = newDevice(id, serverKey, Device1.DeviceStatus.FAILEDNOTALLOWED); writeDevice(nid, device); } private static Device newDevice(final String id, final PublicKey serverKey, final Device1.DeviceStatus status) { String sshEncodedKey = serverKey.toString(); try { sshEncodedKey = AuthorizedKeysDecoder.encodePublicKey(serverKey); } catch (IOException e) { LOG.warn("Unable to encode public key to ssh format.", e); } Device1 d1 = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.FAILEDNOTALLOWED).build(); DeviceBuilder builder = new DeviceBuilder() .setUniqueId(id) .withKey(new DeviceKey(id)) .setSshHostKey(sshEncodedKey) .addAugmentation(Device1.class, d1); return builder.build(); } private Device readAndGetDevice(final NodeId nodeId) { return readDevice(nodeId).orElse(null); } private Optional readDevice(final NodeId nodeId) { ReadTransaction opTx = dataBroker.newReadOnlyTransaction(); InstanceIdentifier deviceIID = buildDeviceInstanceIdentifier(nodeId); ListenableFuture> devFuture = opTx.read(LogicalDatastoreType.OPERATIONAL, deviceIID); try { return devFuture.get(); } catch (InterruptedException | ExecutionException e) { return Optional.empty(); } } private void writeDevice(final NodeId nodeId, final Device modifiedDevice) { WriteTransaction opTx = dataBroker.newWriteOnlyTransaction(); opTx.merge(LogicalDatastoreType.OPERATIONAL, buildDeviceInstanceIdentifier(nodeId), modifiedDevice); commit(opTx, modifiedDevice.key()); } private static InstanceIdentifier buildDeviceInstanceIdentifier(final NodeId nodeId) { return InstanceIdentifier.create(NetconfCallhomeServer.class) .child(AllowedDevices.class) .child(Device.class, new DeviceKey(nodeId.getValue())); } private static Device withConnectedStatus(final Device opDev) { Device1 status = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.CONNECTED).build(); return new DeviceBuilder().addAugmentation(Device1.class, status).setUniqueId(opDev.getUniqueId()) .setSshHostKey(opDev.getSshHostKey()).build(); } private static Device withFailedStatus(final Device opDev) { Device1 status = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.FAILED).build(); return new DeviceBuilder().addAugmentation(Device1.class, status).setUniqueId(opDev.getUniqueId()) .setSshHostKey(opDev.getSshHostKey()).build(); } private static Device withDisconnectedStatus(final Device opDev) { Device1 status = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.DISCONNECTED).build(); return new DeviceBuilder().addAugmentation(Device1.class, status).setUniqueId(opDev.getUniqueId()) .setSshHostKey(opDev.getSshHostKey()).build(); } private static Device withFailedAuthStatus(final Device opDev) { Device1 status = new Device1Builder().setDeviceStatus(Device1.DeviceStatus.FAILEDAUTHFAILURE).build(); return new DeviceBuilder().addAugmentation(Device1.class, status).setUniqueId(opDev.getUniqueId()) .setSshHostKey(opDev.getSshHostKey()).build(); } private void setDeviceStatus(final Device device) { WriteTransaction tx = dataBroker.newWriteOnlyTransaction(); InstanceIdentifier deviceIId = InstanceIdentifier.create(NetconfCallhomeServer.class) .child(AllowedDevices.class) .child(Device.class, device.key()); tx.merge(LogicalDatastoreType.OPERATIONAL, deviceIId, device); commit(tx, device.key()); } private static void commit(WriteTransaction tx, DeviceKey device) { tx.commit().addCallback(new FutureCallback() { @Override public void onSuccess(CommitInfo result) { LOG.debug("Device {} committed", device); } @Override public void onFailure(Throwable cause) { LOG.warn("Failed to commit device {}", device, cause); } }, MoreExecutors.directExecutor()); } private AllowedDevices getDevices() { ReadTransaction rxTransaction = dataBroker.newReadOnlyTransaction(); ListenableFuture> devicesFuture = rxTransaction.read(LogicalDatastoreType.OPERATIONAL, IetfZeroTouchCallHomeServerProvider.ALL_DEVICES); try { return devicesFuture.get().orElse(null); } catch (ExecutionException | InterruptedException e) { LOG.error("Error trying to read the whitelist devices: {}", e); return null; } } private List getDevicesAsList() { AllowedDevices devices = getDevices(); return devices == null ? new ArrayList<>() : devices.getDevice(); } @Override public void reportFailedAuth(final PublicKey sshKey) { AuthorizedKeysDecoder decoder = new AuthorizedKeysDecoder(); for (Device device : getDevicesAsList()) { String keyString = device.getSshHostKey(); try { PublicKey pubKey = decoder.decodePublicKey(keyString); if (sshKey.getAlgorithm().equals(pubKey.getAlgorithm()) && sshKey.equals(pubKey)) { Device failedDevice = withFailedAuthStatus(device); if (failedDevice == null) { return; } LOG.info("Setting auth failed status for callhome device id:{}.", failedDevice.getUniqueId()); setDeviceStatus(failedDevice); return; } } catch (GeneralSecurityException e) { LOG.error("Failed decoding a device key with host key: {}", keyString, e); return; } } LOG.error("No match found for the failed auth device (should have been filtered by whitelist). Key: {}", sshKey); } @Override public void close() { reg.close(); } }