From: Balaji Date: Wed, 11 Jan 2017 17:04:27 +0000 (-0800) Subject: BUG-1422 Introduce Call-Home functionality for the NETCONF Topology. X-Git-Tag: release/carbon~32^2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=a4435c189593770de81621c9cd6de261633030ca;p=netconf.git BUG-1422 Introduce Call-Home functionality for the NETCONF Topology. Introduce Call-Home reversed SSH support. Basic support for host key based white-list and authorization by username/password. Rebased with master; some code flaws fixed Addressed minor code fixes Addressed most of Tomas' review issues Addressed Jakub's review issues Addressed more or Tomas' review issues Removed mistaken hoisting of odl-netconf-callhome-ssh to be topmost feature Reenabled "ignored" unit tests Change-Id: If3e3c9fdd0af0c97043f8930b7f922586863cabf Signed-off-by: Balaji Signed-off-by: Mike Arsenault Signed-off-by: Allan Clarke Signed-off-by: Ryan Goulding --- diff --git a/features/netconf-connector/features-netconf-connector/pom.xml b/features/netconf-connector/features-netconf-connector/pom.xml index 0750c52c2d..9e90e29096 100644 --- a/features/netconf-connector/features-netconf-connector/pom.xml +++ b/features/netconf-connector/features-netconf-connector/pom.xml @@ -123,6 +123,18 @@ ${project.groupId} netconf-config + + ${project.groupId} + callhome-model + + + ${project.groupId} + callhome-protocol + + + ${project.groupId} + callhome-provider + diff --git a/features/netconf-connector/features-netconf-connector/src/main/features/features.xml b/features/netconf-connector/features-netconf-connector/src/main/features/features.xml index c6a4263fbf..a3d60f24ca 100644 --- a/features/netconf-connector/features-netconf-connector/src/main/features/features.xml +++ b/features/netconf-connector/features-netconf-connector/src/main/features/features.xml @@ -19,6 +19,7 @@ odl-netconf-connector odl-netconf-connector-ssh + odl-netconf-callhome-ssh @@ -46,6 +47,13 @@ mvn:org.opendaylight.netconf/netconf-connector-config/{{VERSION}} + + odl-netconf-topology + mvn:org.opendaylight.netconf/callhome-protocol/{{VERSION}} + mvn:org.opendaylight.netconf/callhome-provider/{{VERSION}} + mvn:org.opendaylight.netconf/callhome-model/{{VERSION}} + + odl-netconf-ssh odl-netconf-connector diff --git a/features/netconf-connector/features4-netconf-connector/pom.xml b/features/netconf-connector/features4-netconf-connector/pom.xml index 6b5b5372f7..32ef0d3783 100644 --- a/features/netconf-connector/features4-netconf-connector/pom.xml +++ b/features/netconf-connector/features4-netconf-connector/pom.xml @@ -59,6 +59,13 @@ xml features + + ${project.groupId} + odl-netconf-callhome-ssh + ${project.version} + xml + features + ${project.groupId} odl-netconf-console diff --git a/features/netconf-connector/odl-netconf-callhome-ssh/pom.xml b/features/netconf-connector/odl-netconf-callhome-ssh/pom.xml new file mode 100644 index 0000000000..d66b6ca80e --- /dev/null +++ b/features/netconf-connector/odl-netconf-callhome-ssh/pom.xml @@ -0,0 +1,57 @@ + + + + 4.0.0 + + + org.opendaylight.odlparent + single-feature-parent + 1.8.0-SNAPSHOT + + + + org.opendaylight.netconf + odl-netconf-callhome-ssh + 1.2.0-SNAPSHOT + feature + + OpenDaylight :: Netconf Connector :: Netconf Callhome Connector + Netconf SSH Server + loopback connection configuration + + + + ${project.groupId} + odl-netconf-topology + ${project.version} + xml + features + + + ${project.groupId} + netconf-connector-config + ${project.version} + + + ${project.groupId} + callhome-model + ${project.version} + + + ${project.groupId} + callhome-protocol + ${project.version} + + + ${project.groupId} + callhome-provider + ${project.version} + + + \ No newline at end of file diff --git a/features/netconf-connector/odl-netconf-connector-all/pom.xml b/features/netconf-connector/odl-netconf-connector-all/pom.xml index 7f0ebe0aa5..83c52e39b4 100644 --- a/features/netconf-connector/odl-netconf-connector-all/pom.xml +++ b/features/netconf-connector/odl-netconf-connector-all/pom.xml @@ -40,5 +40,12 @@ xml features + + ${project.groupId} + odl-netconf-callhome-ssh + ${project.version} + xml + features + \ No newline at end of file diff --git a/features/netconf-connector/pom.xml b/features/netconf-connector/pom.xml index 2928bb3aa5..2081582ce3 100644 --- a/features/netconf-connector/pom.xml +++ b/features/netconf-connector/pom.xml @@ -11,7 +11,7 @@ 4.0.0 org.opendaylight.odlparent - odlparent-lite + odlparent 1.8.0-SNAPSHOT @@ -20,6 +20,18 @@ 1.2.0-SNAPSHOT pom + + 1.8.0-SNAPSHOT + 1.5.0-SNAPSHOT + 0.6.0-SNAPSHOT + 1.8.0-SNAPSHOT + 2.2.0-SNAPSHOT + 0.10.0-SNAPSHOT + 1.2.0-SNAPSHOT + 1.5.0-SNAPSHOT + 1.1.0-SNAPSHOT + + features-netconf-connector features4-netconf-connector @@ -28,14 +40,122 @@ odl-netconf-connector odl-netconf-connector-all odl-netconf-connector-ssh + odl-netconf-callhome-ssh odl-netconf-console odl-netconf-topology - - scm:git:http://git.opendaylight.org/gerrit/controller.git - scm:git:ssh://git.opendaylight.org:29418/controller.git - HEAD - https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=summary - + + + + org.opendaylight.netconf + netconf-artifacts + 1.2.0-SNAPSHOT + pom + import + + + + + + + org.opendaylight.yangtools + features-yangtools + ${yangtools.version} + features + xml + + + org.opendaylight.controller + features-mdsal + ${controller.mdsal.version} + features + xml + runtime + + + org.opendaylight.mdsal.model + features-mdsal-model + ${mdsal.model.version} + features + xml + runtime + + + ${project.groupId} + features-netconf + features + xml + + + ${project.groupId} + sal-netconf-connector + + + org.opendaylight.netconf + messagebus-netconf + + + ${project.groupId} + netconf-console + ${project.version} + + + ${project.groupId} + netconf-topology + + + ${project.groupId} + netconf-topology-config + + + ${project.groupId} + netconf-tcp + + + ${project.groupId} + netconf-ssh + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + ${project.groupId} + netconf-topology-singleton + + + ${project.groupId} + callhome-model + + + ${project.groupId} + callhome-provider + + + ${project.groupId} + callhome-protocol + + + ${project.groupId} + netconf-connector-config + + + ${project.groupId} + netconf-config + + + + + scm:git:http://git.opendaylight.org/gerrit/controller.git + scm:git:ssh://git.opendaylight.org:29418/controller.git + HEAD + https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=summary + diff --git a/features/netconf/features-netconf/pom.xml b/features/netconf/features-netconf/pom.xml index 0eb098842b..ab170df881 100644 --- a/features/netconf/features-netconf/pom.xml +++ b/features/netconf/features-netconf/pom.xml @@ -248,6 +248,18 @@ org.opendaylight.netconf mdsal-netconf-impl + + ${project.groupId} + callhome-model + + + ${project.groupId} + callhome-protocol + + + ${project.groupId} + callhome-provider + diff --git a/netconf/callhome-model/pom.xml b/netconf/callhome-model/pom.xml new file mode 100644 index 0000000000..8756b94893 --- /dev/null +++ b/netconf/callhome-model/pom.xml @@ -0,0 +1,24 @@ + + + + 4.0.0 + + + org.opendaylight.mdsal + binding-parent + 0.10.0-SNAPSHOT + + + + org.opendaylight.netconf + callhome-model + 1.2.0-SNAPSHOT + bundle + ${project.artifactId} + diff --git a/netconf/callhome-model/src/main/yang/odl-netconf-callhome-server.yang b/netconf/callhome-model/src/main/yang/odl-netconf-callhome-server.yang new file mode 100644 index 0000000000..52a9b0f4c3 --- /dev/null +++ b/netconf/callhome-model/src/main/yang/odl-netconf-callhome-server.yang @@ -0,0 +1,65 @@ +module odl-netconf-callhome-server { + + namespace "urn:opendaylight:params:xml:ns:yang:netconf-callhome-server"; + prefix "callhome"; + + organization + "OpenDaylight Project"; + + contact + "netconf-dev@lists.opendaylight.org"; + + description + "This module defines the northbound interface for OpenDaylight NETCONF Callhome."; + + revision "2016-11-09" { + description "Initial version"; + } + + grouping credentials { + container credentials { + presence "Credentials to device."; + leaf username { + mandatory true; + description "Username to be used for authentication"; + type string { + length "1..max"; + } + } + leaf-list passwords { + description "Passwords to be used for authentication."; + type string; + } + } + } + + container netconf-callhome-server { + description "Settings for call home server administration"; + + container global { + presence "global credentials are enabled."; + uses credentials; + leaf accept-all-ssh-keys { + type boolean; + default false; + } + } + + container allowed-devices { + description "A list of allowed devices"; + list device { + key unique-id; + leaf unique-id { + description "Identifier of device, which will be used to identify device."; + type string; + } + leaf ssh-host-key { + description "BASE-64 encoded public key which device will use during connection."; + type string; + } + unique ssh-host-key; + uses credentials; + } + } + } +} diff --git a/netconf/callhome-protocol/pom.xml b/netconf/callhome-protocol/pom.xml new file mode 100644 index 0000000000..3e68f86bf8 --- /dev/null +++ b/netconf/callhome-protocol/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + org.opendaylight.mdsal + binding-parent + 0.10.0-SNAPSHOT + + + + org.opendaylight.netconf + callhome-protocol + 1.2.0-SNAPSHOT + ${project.artifactId} + bundle + + + + + org.opendaylight.netconf + netconf-subsystem + ${project.version} + pom + import + + + + + + + org.opendaylight.netconf + netconf-client + + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + + + com.google.guava + guava + + + org.slf4j + slf4j-api + + + org.opendaylight.yangtools + mockito-configuration + test + + + + + diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoder.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoder.java new file mode 100644 index 0000000000..ff6d380c73 --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoder.java @@ -0,0 +1,132 @@ +/* + * 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.protocol; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.apache.sshd.common.util.Base64; +import org.apache.sshd.common.util.SecurityUtils; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.ECPointUtil; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; + +/** + * + * FIXME: This should be probably located at AAA library + * + */ +public class AuthorizedKeysDecoder { + + private static final String KEY_FACTORY_TYPE_RSA = "RSA"; + private static final String KEY_FACTORY_TYPE_DSA = "DSA"; + private static final String KEY_FACTORY_TYPE_ECDSA = "EC"; + + private static Map ECDSA_CURVES = new HashMap<>(); + static { + ECDSA_CURVES.put("nistp256", "secp256r1"); + ECDSA_CURVES.put("nistp384", "secp384r1"); + ECDSA_CURVES.put("nistp512", "secp512r1"); + } + private static String ECDSA_SUPPORTED_CURVE_NAME = "nistp256"; + private static String ECDSA_SUPPORTED_CURVE_NAME_SPEC = ECDSA_CURVES.get(ECDSA_SUPPORTED_CURVE_NAME); + + private static final String KEY_TYPE_RSA = "ssh-rsa"; + private static final String KEY_TYPE_DSA = "ssh-dss"; + private static final String KEY_TYPE_ECDSA = "ecdsa-sha2-" + ECDSA_SUPPORTED_CURVE_NAME; + + private byte[] bytes = new byte[0]; + private int pos = 0; + + + public PublicKey decodePublicKey(String keyLine) throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException { + + // look for the Base64 encoded part of the line to decode + // both ssh-rsa and ssh-dss begin with "AAAA" due to the length bytes + bytes = Base64.decodeBase64(keyLine.getBytes()); + if (bytes.length == 0) + throw new IllegalArgumentException("No Base64 part to decode in " + keyLine); + pos = 0; + + String type = decodeType(); + if (type.equals(KEY_TYPE_RSA)) + return decodeAsRSA(); + + if (type.equals(KEY_TYPE_DSA)) + return decodeAsDSA(); + + if(type.equals(KEY_TYPE_ECDSA)) + return decodeAsECDSA(); + + throw new IllegalArgumentException("Unknown decode key type " + type + " in " + keyLine); + } + + private PublicKey decodeAsECDSA() + throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + KeyFactory ecdsaFactory = SecurityUtils.getKeyFactory(KEY_FACTORY_TYPE_ECDSA); + + ECNamedCurveParameterSpec spec256r1 = ECNamedCurveTable.getParameterSpec(ECDSA_SUPPORTED_CURVE_NAME_SPEC); + ECNamedCurveSpec params256r1 = new ECNamedCurveSpec(ECDSA_SUPPORTED_CURVE_NAME_SPEC, spec256r1.getCurve(), spec256r1.getG(), spec256r1.getN()); + // copy last 65 bytes from ssh key. + ECPoint point = ECPointUtil.decodePoint(params256r1.getCurve(), Arrays.copyOfRange(bytes,39,bytes.length)); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params256r1); + + return ecdsaFactory.generatePublic(pubKeySpec); + } + + private PublicKey decodeAsDSA() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + KeyFactory dsaFactory = SecurityUtils.getKeyFactory(KEY_FACTORY_TYPE_DSA); + BigInteger p = decodeBigInt(); + BigInteger q = decodeBigInt(); + BigInteger g = decodeBigInt(); + BigInteger y = decodeBigInt(); + DSAPublicKeySpec spec = new DSAPublicKeySpec(y, p, q, g); + + return dsaFactory.generatePublic(spec); + } + + private PublicKey decodeAsRSA() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { + KeyFactory rsaFactory = SecurityUtils.getKeyFactory(KEY_FACTORY_TYPE_RSA); + BigInteger exponent = decodeBigInt(); + BigInteger modulus = decodeBigInt(); + RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); + + return rsaFactory.generatePublic(spec); + } + + private String decodeType() { + int len = decodeInt(); + String type = new String(bytes, pos, len); + pos += len; + return type; + } + + private int decodeInt() { + return ((bytes[pos++] & 0xFF) << 24) | ((bytes[pos++] & 0xFF) << 16) + | ((bytes[pos++] & 0xFF) << 8) | (bytes[pos++] & 0xFF); + } + + private BigInteger decodeBigInt() { + int len = decodeInt(); + byte[] bigIntBytes = new byte[len]; + System.arraycopy(bytes, pos, bigIntBytes, 0, len); + pos += len; + return new BigInteger(bigIntBytes); + } +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorization.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorization.java new file mode 100644 index 0000000000..9cce1735f2 --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorization.java @@ -0,0 +1,181 @@ +/* + * 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.protocol; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import java.security.KeyPair; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import org.apache.sshd.ClientSession; +import org.apache.sshd.client.session.ClientSessionImpl; + +/** + * + * Authorization context for incoming call home sessions. + * + * @see CallHomeAuthorizationProvider + */ +public abstract class CallHomeAuthorization { + private static final CallHomeAuthorization REJECTED = new CallHomeAuthorization() { + + @Override + public boolean isServerAllowed() { + return false; + } + + @Override + protected String getSessionName(){ + return ""; + } + + @Override + protected void applyTo(ClientSession session) { + throw new IllegalStateException("Server is not allowed."); + } + }; + + /** + * + * Returns CallHomeAuthorization object with intent to + * reject incoming connection. + * + * {@link CallHomeAuthorizationProvider} may use returned object + * as return value for {@link CallHomeAuthorizationProvider#provideAuth(java.net.SocketAddress, java.security.PublicKey)} + * if the incoming session should be rejected due to policy implemented + * by provider. + * + * @return CallHomeAuthorization with {@code isServerAllowed() == false} + */ + public static final CallHomeAuthorization rejected() { + return REJECTED; + } + + /** + * Creates a builder for CallHomeAuthorization with intent + * to accept incoming connection and to provide credentials. + * + * Note: If session with same sessionName is already opened and + * active, incoming session will be rejected. + * + * @param sessionName Application specific unique identifier for incoming session + * @param username Username to be used for authorization + * @return Builder which allows to specify credentials. + */ + public static final Builder serverAccepted(String sessionName, String username) { + return new Builder(sessionName, username); + } + + /** + * Returns true if incomming connection is allowed. + * + * @return true if incoming connection from SSH Server is allowed. + */ + public abstract boolean isServerAllowed(); + + /** + * + * Applies provided authentification to Mina SSH Client Session + * + * @param session Client Session to which authorization parameters will by applied + */ + protected abstract void applyTo(ClientSession session); + + protected abstract String getSessionName(); + + /** + * + * Builder for CallHomeAuthorization which accepts incoming connection. + * + * Use {@link CallHomeAuthorization#serverAccepted(String, String)} to instantiate + * builder. + * + */ + public static class Builder implements org.opendaylight.yangtools.concepts.Builder { + + private final String nodeId; + private final String username; + private Set passwords = new HashSet<>(); + private Set clientKeys = new HashSet<>(); + + private Builder(String nodeId, String username) { + this.nodeId = Preconditions.checkNotNull(nodeId); + this.username = Preconditions.checkNotNull(username); + } + + /** + * + * Adds password, which will be used for password-based authorization. + * + * @param password Password to be used for password-based authorization. + * @return this builder. + */ + public Builder addPassword(String password) { + this.passwords.add(password); + return this; + } + + /** + * + * Adds public / private key pair to be used for public-key based authorization. + * + * @param clientKey Keys to be used for authorization. + * @return this builder. + */ + public Builder addClientKeys(KeyPair clientKey){ + this.clientKeys.add(clientKey); + return this; + } + + @Override + public CallHomeAuthorization build() { + return new ServerAllowed(nodeId, username, passwords, clientKeys); + } + + } + + private static class ServerAllowed extends CallHomeAuthorization { + + private final String nodeId; + private final String username; + private final Set passwords; + private final Set clientKeyPair; + + ServerAllowed(String nodeId, String username, Collectionpasswords, Collection clientKeyPairs) { + this.username = Preconditions.checkNotNull(username); + this.passwords = ImmutableSet.copyOf(passwords); + this.clientKeyPair = ImmutableSet.copyOf(clientKeyPairs); + this.nodeId = Preconditions.checkNotNull(nodeId); + } + + @Override + protected String getSessionName() { + return nodeId; + } + + @Override + public boolean isServerAllowed() { + return true; + } + + @Override + protected void applyTo(ClientSession session) { + Preconditions.checkArgument(session instanceof ClientSessionImpl); + ((ClientSessionImpl) session).setUsername(username); + + // First try authentication using server host keys, else try password. + for (KeyPair keyPair : clientKeyPair) { + session.addPublicKeyIdentity(keyPair); + } + for (String password : passwords) { + session.addPasswordIdentity(password); + } + } + } +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationProvider.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationProvider.java new file mode 100644 index 0000000000..a07a925bfc --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationProvider.java @@ -0,0 +1,34 @@ +/* + * 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.protocol; + +import java.net.SocketAddress; +import java.security.PublicKey; +import javax.annotation.Nonnull; + +/** + * + * Provider responsible for resolving CallHomeAuthorization + * + */ +public interface CallHomeAuthorizationProvider { + + /** + * + * Provides authorization parameters for incoming call-home connection. + * + * @param remoteAddress Remote socket address of incoming connection + * @param serverKey SSH key provided by SSH server on incoming connection + * + * @return {@link CallHomeAuthorization} with authorization information. + * + */ + @Nonnull + CallHomeAuthorization provideAuth(@Nonnull SocketAddress remoteAddress,@Nonnull PublicKey serverKey); + +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeChannelActivator.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeChannelActivator.java new file mode 100644 index 0000000000..f64ffc34de --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeChannelActivator.java @@ -0,0 +1,34 @@ +/* + * 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.protocol; + +import io.netty.util.concurrent.Promise; +import javax.annotation.Nonnull; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.netconf.client.NetconfClientSessionListener; + +/** + * Activator of NETCONF channel on incoming SSH Call Home session. + * + */ +public interface CallHomeChannelActivator { + + /** + * + * Activates Netconf Client Channel with supplied client session listener. + * + * Activation of channel will result in start of NETCONF client + * session negotiation on underlying ssh channel. + * + * @param listener Client Session Listener to be attached to NETCONF session. + * @return Promise with negotiated NETCONF session + */ + @Nonnull + Promise activate(@Nonnull NetconfClientSessionListener listener); + +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeNetconfSubsystemListener.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeNetconfSubsystemListener.java new file mode 100644 index 0000000000..74d9344093 --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeNetconfSubsystemListener.java @@ -0,0 +1,31 @@ +/* + * 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.protocol; + +import org.opendaylight.netconf.client.NetconfClientSessionListener; + +/** + * + * Listener for successful opening of NETCONF channel on incoming Call Home connections. + * + */ +public interface CallHomeNetconfSubsystemListener { + + /** + * Invoked when Netconf Subsystem was successfully opened on incoming SSH Call Home connection. + * + * Implementors of this method should use provided {@link CallHomeChannelActivator} to attach + * {@link NetconfClientSessionListener} to session and to start NETCONF client session negotiation. + * + * + * @param session Incoming Call Home session on which NETCONF subsystem was successfully opened + * @param activator Channel Activator to be used in order to start NETCONF Session negotiation. + */ + void onNetconfSubsystemOpened(CallHomeProtocolSessionContext session, CallHomeChannelActivator activator); + +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeProtocolSessionContext.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeProtocolSessionContext.java new file mode 100644 index 0000000000..a2e1cb1ec8 --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeProtocolSessionContext.java @@ -0,0 +1,49 @@ +/* + * 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.protocol; + +import java.net.InetSocketAddress; +import java.security.PublicKey; + +/** + * + * Protocol level Session Context for incoming Call Home connections. + * + */ +public interface CallHomeProtocolSessionContext { + + /** + * Returns session identifier provided by CallHomeAuthorizationProvider + * + * @return Returns application-provided session identifier + */ + String getSessionName(); + + /** + * Returns public key provided by remote SSH Server for this session. + * + * @return public key provided by remote SSH Server + */ + PublicKey getRemoteServerKey(); + + /** + * + * Returns remote socket address associated with this session. + * + * @return remote socket address associated with this session. + */ + InetSocketAddress getRemoteAddress(); + + /** + * Returns version string provided by remote server. + * + * @return Version string provided by remote server. + */ + String getRemoteServerVersion(); + +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContext.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContext.java new file mode 100644 index 0000000000..8d844c7c8e --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContext.java @@ -0,0 +1,196 @@ +/* + * 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.protocol; + +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import io.netty.channel.EventLoopGroup; +import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.concurrent.Promise; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.PublicKey; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import org.apache.sshd.ClientChannel; +import org.apache.sshd.ClientSession; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.client.future.OpenFuture; +import org.apache.sshd.client.session.ClientSessionImpl; +import org.apache.sshd.common.Session; +import org.apache.sshd.common.future.SshFutureListener; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.netconf.client.NetconfClientSessionListener; +import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class CallHomeSessionContext implements CallHomeProtocolSessionContext { + + private static final Logger LOG = LoggerFactory.getLogger(CallHomeSessionContext.class); + static final Session.AttributeKey SESSION_KEY = new Session.AttributeKey<>(); + + private static final String NETCONF = "netconf"; + + private final ClientSessionImpl sshSession; + private final CallHomeAuthorization authorization; + private final Factory factory; + + private volatile MinaSshNettyChannel nettyChannel = null; + private volatile boolean activated; + + private InetSocketAddress remoteAddress; + private PublicKey serverKey; + + CallHomeSessionContext(ClientSession sshSession, CallHomeAuthorization authorization, SocketAddress remoteAddress, + Factory factory) { + this.authorization = Preconditions.checkNotNull(authorization, "authorization"); + Preconditions.checkArgument(this.authorization.isServerAllowed(), "Server was not allowed."); + Preconditions.checkArgument(sshSession instanceof ClientSessionImpl, + "sshSession must implement ClientSessionImpl"); + this.factory = Preconditions.checkNotNull(factory, "factory"); + this.sshSession = (ClientSessionImpl) sshSession; + this.sshSession.setAttribute(SESSION_KEY, this); + this.remoteAddress = (InetSocketAddress) this.sshSession.getIoSession().getRemoteAddress(); + this.serverKey = this.sshSession.getKex().getServerKey(); + } + + static CallHomeSessionContext getFrom(ClientSession sshSession) { + return sshSession.getAttribute(SESSION_KEY); + } + + AuthFuture authorize() throws IOException { + authorization.applyTo(sshSession); + return sshSession.auth(); + } + + void openNetconfChannel() { + LOG.debug("Opening NETCONF Subsystem on {}", sshSession); + try { + final ClientChannel netconfChannel = sshSession.createSubsystemChannel(NETCONF); + netconfChannel.setStreaming(ClientChannel.Streaming.Async); + netconfChannel.open().addListener(newSshFutureListener(netconfChannel)); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + SshFutureListener newSshFutureListener(final ClientChannel netconfChannel) { + return future -> + { + if (future.isOpened()) { + netconfChannelOpened(netconfChannel); + } else { + channelOpenFailed(future.getException()); + } + }; + } + + private void channelOpenFailed(Throwable e) { + LOG.error("Unable to open netconf subsystem, disconnecting.", e); + sshSession.close(false); + } + + private void netconfChannelOpened(ClientChannel netconfChannel) { + nettyChannel = newMinaSshNettyChannel(netconfChannel); + factory.getChannelOpenListener().onNetconfSubsystemOpened(CallHomeSessionContext.this, + listener -> doActivate(listener)); + } + + @GuardedBy("this") + private synchronized Promise doActivate(NetconfClientSessionListener listener) { + if(activated) { + return newSessionPromise().setFailure(new IllegalStateException("Session already activated.")); + } + activated = true; + LOG.info("Activating Netconf channel for {} with {}", getRemoteAddress(), listener); + Promise activationPromise = newSessionPromise(); + factory.getChannelInitializer(listener).initialize(nettyChannel, activationPromise); + factory.getNettyGroup().register(nettyChannel).awaitUninterruptibly(500); + return activationPromise; + } + + protected MinaSshNettyChannel newMinaSshNettyChannel(ClientChannel netconfChannel) { + return new MinaSshNettyChannel(this, sshSession, netconfChannel); + } + + private Promise newSessionPromise() { + return GlobalEventExecutor.INSTANCE.newPromise(); + } + + @Override + public PublicKey getRemoteServerKey() { + return serverKey; + } + + @Override + public String getRemoteServerVersion() { + return sshSession.getServerVersion(); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return remoteAddress; + } + + @Override + public String getSessionName() { + return authorization.getSessionName(); + } + + void removeSelf() { + factory.remove(this); + } + + static class Factory { + + private final EventLoopGroup nettyGroup; + private final NetconfClientSessionNegotiatorFactory negotiatorFactory; + private final CallHomeNetconfSubsystemListener subsystemListener; + private final ConcurrentMap sessions = new ConcurrentHashMap<>(); + + Factory(EventLoopGroup nettyGroup, NetconfClientSessionNegotiatorFactory negotiatorFactory, + CallHomeNetconfSubsystemListener subsystemListener) { + this.nettyGroup = Preconditions.checkNotNull(nettyGroup, "nettyGroup"); + this.negotiatorFactory = Preconditions.checkNotNull(negotiatorFactory, "negotiatorFactory"); + this.subsystemListener = Preconditions.checkNotNull(subsystemListener); + } + + void remove(CallHomeSessionContext session) { + sessions.remove(session.getSessionName(), session); + } + + ReverseSshChannelInitializer getChannelInitializer(NetconfClientSessionListener listener) { + return ReverseSshChannelInitializer.create(negotiatorFactory, listener); + } + + CallHomeNetconfSubsystemListener getChannelOpenListener() { + return this.subsystemListener; + } + + @Nullable + CallHomeSessionContext createIfNotExists(ClientSession sshSession, CallHomeAuthorization authorization, + SocketAddress remoteAddress) { + CallHomeSessionContext session = new CallHomeSessionContext(sshSession, authorization, remoteAddress, this); + CallHomeSessionContext preexisting = sessions.putIfAbsent(session.getSessionName(), session); + // If preexisting is null - session does not exist, so we can safely create new one, otherwise we return + // null and incoming connection will be rejected. + return preexisting == null ? session : null; + } + + EventLoopGroup getNettyGroup() { + return nettyGroup; + } + + } + +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannel.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannel.java new file mode 100644 index 0000000000..e5f5f95a22 --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannel.java @@ -0,0 +1,187 @@ +/* + * 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.protocol; + +import com.google.common.base.Preconditions; +import io.netty.buffer.ByteBuf; +import io.netty.channel.AbstractServerChannel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelConfig; +import io.netty.channel.EventLoop; +import java.net.SocketAddress; +import org.apache.sshd.ClientChannel; +import org.apache.sshd.ClientSession; +import org.opendaylight.netconf.nettyutil.handler.ssh.client.AsyncSshHandlerReader; +import org.opendaylight.netconf.nettyutil.handler.ssh.client.AsyncSshHandlerReader.ReadMsgHandler; +import org.opendaylight.netconf.nettyutil.handler.ssh.client.AsyncSshHandlerWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class MinaSshNettyChannel extends AbstractServerChannel { + + private static final Logger LOG = LoggerFactory.getLogger(MinaSshNettyChannel.class); + private static final ChannelMetadata METADATA = new ChannelMetadata(false); + private final ChannelConfig config = new DefaultChannelConfig(this); + private final CallHomeSessionContext context; + private final ClientSession session; + private final ClientChannel sshChannel; + private final AsyncSshHandlerReader sshReadHandler; + private final AsyncSshHandlerWriter sshWriteAsyncHandler; + + + private volatile boolean nettyClosed = false; + + MinaSshNettyChannel(CallHomeSessionContext context, ClientSession session, ClientChannel sshChannel) { + this.context = Preconditions.checkNotNull(context); + this.session = Preconditions.checkNotNull(session); + this.sshChannel = Preconditions.checkNotNull(sshChannel); + this.sshReadHandler = new AsyncSshHandlerReader(new ConnectionClosedDuringRead(), new FireReadMessage(), "netconf", + sshChannel.getAsyncOut()); + this.sshWriteAsyncHandler = new AsyncSshHandlerWriter(sshChannel.getAsyncIn()); + pipeline().addFirst(createChannelAdapter()); + } + + private ChannelOutboundHandlerAdapter createChannelAdapter() { + return new ChannelOutboundHandlerAdapter() { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + sshWriteAsyncHandler.write(ctx, msg, promise); + } + + }; + } + + @Override + public ChannelConfig config() { + return config; + } + + private boolean notClosing(org.apache.sshd.common.Closeable sshCloseable) { + return !sshCloseable.isClosing() && !sshCloseable.isClosed(); + } + + + @Override + public boolean isOpen() { + return notClosing(session); + } + + @Override + public boolean isActive() { + return notClosing(session); + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + @Override + protected AbstractUnsafe newUnsafe() { + return new SshUnsafe(); + } + + @Override + protected boolean isCompatible(EventLoop loop) { + return true; + } + + @Override + protected SocketAddress localAddress0() { + return session.getIoSession().getLocalAddress(); + } + + @Override + protected SocketAddress remoteAddress0() { + return context.getRemoteAddress(); + } + + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + throw new UnsupportedOperationException("Bind not supported."); + } + + void doMinaDisconnect(boolean blocking) { + if(notClosing(session)) { + sshChannel.close(blocking); + session.close(blocking); + } + } + + void doNettyDisconnect() { + if(! nettyClosed) { + nettyClosed = true; + pipeline().fireChannelInactive(); + sshReadHandler.close(); + sshWriteAsyncHandler.close(); + } + } + + @Override + protected void doDisconnect() throws Exception { + LOG.info("Disconnect invoked"); + doNettyDisconnect(); + doMinaDisconnect(false); + } + + @Override + protected void doClose() throws Exception { + context.removeSelf(); + if(notClosing(session)) { + session.close(true); + sshChannel.close(true); + } + } + + @Override + protected void doBeginRead() throws Exception { + // Intentional NOOP - read is started by AsyncSshHandlerReader + } + + @Override + protected void doWrite(ChannelOutboundBuffer in) throws Exception { + throw new IllegalStateException("Outbound writes to SSH should be done by SSH Write handler"); + } + + private final class FireReadMessage implements ReadMsgHandler { + + @Override + public void onMessageRead(ByteBuf msg) { + pipeline().fireChannelRead(msg); + } + + } + + private final class ConnectionClosedDuringRead implements AutoCloseable { + + /** + * Invoked when SSH session dropped during read using {@link AsyncSshHandlerReader} + */ + @Override + public void close() throws Exception { + doNettyDisconnect(); + } + + } + + private class SshUnsafe extends AbstractUnsafe { + + @Override + public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + throw new UnsupportedOperationException("Unsafe is not supported."); + } + + } +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServer.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServer.java new file mode 100644 index 0000000000..460c742245 --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServer.java @@ -0,0 +1,184 @@ +/* + * 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.protocol; + +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.PublicKey; +import org.apache.sshd.ClientSession; +import org.apache.sshd.SshClient; +import org.apache.sshd.client.ServerKeyVerifier; +import org.apache.sshd.client.SessionFactory; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.common.Session; +import org.apache.sshd.common.SessionListener; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoAcceptor; +import org.apache.sshd.common.io.IoServiceFactory; +import org.apache.sshd.common.io.mina.MinaServiceFactory; +import org.apache.sshd.common.io.nio2.Nio2ServiceFactory; +import org.opendaylight.netconf.callhome.protocol.CallHomeSessionContext.Factory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NetconfCallHomeServer implements AutoCloseable, ServerKeyVerifier { + + private static final Logger LOG = LoggerFactory.getLogger(NetconfCallHomeServer.class); + + private final IoAcceptor acceptor; + private final SshClient client; + private final CallHomeAuthorizationProvider authProvider; + private final CallHomeSessionContext.Factory sessionFactory; + private final InetSocketAddress bindAddress; + + NetconfCallHomeServer(SshClient sshClient, CallHomeAuthorizationProvider authProvider, Factory factory, + InetSocketAddress socketAddress) { + this.client = Preconditions.checkNotNull(sshClient); + this.authProvider = Preconditions.checkNotNull(authProvider); + this.sessionFactory = Preconditions.checkNotNull(factory); + this.bindAddress = socketAddress; + + sshClient.setServerKeyVerifier(this); + + SessionFactory clientSessions = new SessionFactory(); + clientSessions.setClient(sshClient); + clientSessions.addListener(createSessionListener()); + + IoServiceFactory minaFactory = createServiceFactory(sshClient); + this.acceptor = minaFactory.createAcceptor(clientSessions); + } + + IoServiceFactory createServiceFactory(SshClient sshClient) { + try { + return createMinaServiceFactory(sshClient); + } catch (NoClassDefFoundError e) { + LOG.warn("Mina is not available, defaulting to NIO."); + return new Nio2ServiceFactory(sshClient); + } + } + + + protected IoServiceFactory createMinaServiceFactory(SshClient sshClient) { + return new MinaServiceFactory(sshClient); + } + + SessionListener createSessionListener() { + return new SessionListener() { + + @Override + public void sessionEvent(Session session, Event event) { + ClientSession cSession = (ClientSession) session; + LOG.debug("SSH session {} event {}", session, event); + switch (event) { + case KeyEstablished: + doAuth(cSession); + break; + case Authenticated: + doPostAuth(cSession); + break; + default: + break; + } + } + + @Override + public void sessionCreated(Session session) { + LOG.debug("SSH session {} created", session); + } + + @Override + public void sessionClosed(Session session) { + CallHomeSessionContext ctx = CallHomeSessionContext.getFrom((ClientSession) session); + if(ctx != null) { + ctx.removeSelf(); + } + LOG.debug("SSH Session {} closed", session); + } + }; + } + + private void doPostAuth(final ClientSession cSession) { + CallHomeSessionContext.getFrom(cSession).openNetconfChannel(); + } + + private void doAuth(final ClientSession cSession) { + try { + final AuthFuture authFuture = CallHomeSessionContext.getFrom(cSession).authorize(); + authFuture.addListener(newAuthSshFutureListener(cSession)); + } catch (IOException e) { + LOG.error("Failed to authorize session {}", cSession, e); + } + } + + SshFutureListener newAuthSshFutureListener(final ClientSession cSession) { + return new SshFutureListener() { + @Override + public void operationComplete(AuthFuture authFuture) { + if (authFuture.isSuccess()) { + onSuccess(); + } else if (authFuture.isFailure()) { + onFailure(authFuture.getException()); + } else if (authFuture.isCanceled()) { + onCanceled(); + } + authFuture.removeListener(this); + } + + private void onSuccess() { + LOG.debug("Authorize success"); + } + + private void onFailure(Throwable throwable) { + LOG.error("Failed to authorize session {}", cSession, throwable); + cSession.close(true); + } + + private void onCanceled() { + LOG.warn("Authorize cancelled"); + cSession.close(true); + } + }; + } + + @Override + public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) { + final CallHomeAuthorization authorization = authProvider.provideAuth(remoteAddress, serverKey); + // server is not authorized + if (!authorization.isServerAllowed()) { + LOG.info("Incoming session {} was rejected by Authorization Provider.",sshClientSession); + return false; + } + CallHomeSessionContext session = sessionFactory.createIfNotExists(sshClientSession, authorization, remoteAddress); + // Session was created, session with same name does not exists + if(session != null) { + return true; + } + // Session was not created, session with same name exists + LOG.info("Incoming session {} was rejected. Session with same name {} is already active.",sshClientSession,authorization.getSessionName()); + return false; + } + + public void bind() throws IOException { + try { + client.start(); + acceptor.bind(bindAddress); + } catch (IOException e) { + LOG.error("Unable to start NETCONF CallHome Service", e); + throw e; + } + } + + + @Override + public void close() throws Exception { + acceptor.close(true); + } +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerBuilder.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerBuilder.java new file mode 100644 index 0000000000..f9252466ba --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerBuilder.java @@ -0,0 +1,127 @@ +/* + * 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.protocol; + +import com.google.common.base.Optional; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.local.LocalEventLoopGroup; +import io.netty.util.HashedWheelTimer; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; +import org.apache.sshd.SshClient; +import org.opendaylight.netconf.api.messages.NetconfHelloMessageAdditionalHeader; +import org.opendaylight.netconf.callhome.protocol.CallHomeSessionContext.Factory; +import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory; +import org.opendaylight.yangtools.concepts.Builder; + +public class NetconfCallHomeServerBuilder implements Builder { + + private static final long DEFAULT_SESSION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5); + private static final int DEFAULT_CALL_HOME_PORT = 6666; + + private SshClient sshClient; + private EventLoopGroup nettyGroup; + private NetconfClientSessionNegotiatorFactory negotiationFactory; + private InetSocketAddress bindAddress; + + private final CallHomeAuthorizationProvider authProvider; + private final CallHomeNetconfSubsystemListener subsystemListener; + + public NetconfCallHomeServerBuilder(CallHomeAuthorizationProvider authProvider, + CallHomeNetconfSubsystemListener subsystemListener) { + this.authProvider = authProvider; + this.subsystemListener = subsystemListener; + } + + @Override + public NetconfCallHomeServer build() { + Factory factory = + new CallHomeSessionContext.Factory(nettyGroup(), negotiatorFactory(), subsystemListener()); + return new NetconfCallHomeServer(sshClient(), authProvider(), factory, bindAddress()); + } + + public SshClient getSshClient() { + return sshClient; + } + + public void setSshClient(SshClient sshClient) { + this.sshClient = sshClient; + } + + public EventLoopGroup getNettyGroup() { + return nettyGroup; + } + + public void setNettyGroup(EventLoopGroup nettyGroup) { + this.nettyGroup = nettyGroup; + } + + public NetconfClientSessionNegotiatorFactory getNegotiationFactory() { + return negotiationFactory; + } + + public void setNegotiationFactory(NetconfClientSessionNegotiatorFactory negotiationFactory) { + this.negotiationFactory = negotiationFactory; + } + + public InetSocketAddress getBindAddress() { + return bindAddress; + } + + public void setBindAddress(InetSocketAddress bindAddress) { + this.bindAddress = bindAddress; + } + + public CallHomeAuthorizationProvider getAuthProvider() { + return authProvider; + } + + + private InetSocketAddress bindAddress() { + return bindAddress != null ? bindAddress : defaultBindAddress(); + } + + private EventLoopGroup nettyGroup() { + return nettyGroup != null ? nettyGroup : defaultNettyGroup(); + } + + private NetconfClientSessionNegotiatorFactory negotiatorFactory() { + return negotiationFactory != null ? negotiationFactory : defaultNegotiationFactory(); + } + + private CallHomeNetconfSubsystemListener subsystemListener() { + return subsystemListener; + } + + private CallHomeAuthorizationProvider authProvider() { + return authProvider; + } + + private SshClient sshClient() { + return sshClient != null ? sshClient : defaultSshClient(); + } + + private SshClient defaultSshClient() { + return SshClient.setUpDefaultClient(); + } + + private NetconfClientSessionNegotiatorFactory defaultNegotiationFactory() { + return new NetconfClientSessionNegotiatorFactory(new HashedWheelTimer(), + Optional.absent(), DEFAULT_SESSION_TIMEOUT_MILLIS); + } + + private EventLoopGroup defaultNettyGroup() { + return new LocalEventLoopGroup(); + } + + private InetSocketAddress defaultBindAddress() { + return new InetSocketAddress(DEFAULT_CALL_HOME_PORT); + } + +} diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/ReverseSshChannelInitializer.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/ReverseSshChannelInitializer.java new file mode 100644 index 0000000000..009cf2170e --- /dev/null +++ b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/ReverseSshChannelInitializer.java @@ -0,0 +1,48 @@ +/* + * 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.protocol; + +import io.netty.channel.Channel; +import io.netty.util.concurrent.Promise; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.netconf.client.NetconfClientSessionListener; +import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory; +import org.opendaylight.netconf.nettyutil.AbstractChannelInitializer; +import org.opendaylight.protocol.framework.SessionListenerFactory; + +class ReverseSshChannelInitializer extends AbstractChannelInitializer + implements SessionListenerFactory { + + private final NetconfClientSessionNegotiatorFactory negotiatorFactory; + private final NetconfClientSessionListener sessionListener; + + private ReverseSshChannelInitializer(NetconfClientSessionNegotiatorFactory negotiatorFactory, + NetconfClientSessionListener sessionListener) { + super(); + this.negotiatorFactory = negotiatorFactory; + this.sessionListener = sessionListener; + } + + public static ReverseSshChannelInitializer create(NetconfClientSessionNegotiatorFactory negotiatorFactory, + NetconfClientSessionListener listener) { + return new ReverseSshChannelInitializer(negotiatorFactory, listener); + } + + @Override + public NetconfClientSessionListener getSessionListener() { + return sessionListener; + } + + @Override + protected void initializeSessionNegotiator(Channel ch, Promise promise) { + ch.pipeline().addAfter(NETCONF_MESSAGE_DECODER, AbstractChannelInitializer.NETCONF_SESSION_NEGOTIATOR, + negotiatorFactory.getSessionNegotiator(this, ch, promise)); + } + +} diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoderTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoderTest.java new file mode 100644 index 0000000000..20cab640cd --- /dev/null +++ b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoderTest.java @@ -0,0 +1,97 @@ +/* + * 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.protocol; + +import static org.junit.Assert.assertEquals; + +import java.security.GeneralSecurityException; +import java.security.PublicKey; + +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.netconf.callhome.protocol.AuthorizedKeysDecoder; + + +public class AuthorizedKeysDecoderTest { + + AuthorizedKeysDecoder instance; + + @Before + public void setup() { + instance = new AuthorizedKeysDecoder(); + } + + @Test + public void authorizedKeysDecoderValidRSAKey () throws GeneralSecurityException { + // given + String rsaStr = "AAAAB3NzaC1yc2EAAAADAQABAAABAQCvLigTfPZMqOQwHp051Co4lwwPwO21NFIXWgjQmCPEgRTqQpei7qQaxlLGkrIPjZtJQRgCuC+Sg8HFw1YpUaMybN0nFInInQLp/qe0yc9ByDZM2G86NX6W5W3+j87I8Fh1dnMov1iJ0DFVn8RLwdEGjreiZCRyJOMuHghh6y4EG7W8BwmZrse17zhSpc2wFOVhxeZnYAQFEw6g48LutFRDpoTjGgz1nz/L4zcaUxxigs8wdY+qTTOHxSTxlLqwSZPFLyYrV2KJ9mKahMuYUy6o2b8snsjvnSjyK0kY+U0C6c8fmPDFUc0RqJqfdnsIUyh11U8d3NZdaFWg0UW0SNK3"; + // when + PublicKey serverKey = instance.decodePublicKey(rsaStr); + // then + assertEquals(serverKey.getAlgorithm(), "RSA"); + } + + @Test(expected = Exception.class) + public void authorizedKeysDecoderInvalidRSAKey () throws GeneralSecurityException { + // given + String rsaStr = "AAAB3NzaC1yc2EAAAADAQABAAABAQCvLigTfPZMqOQwHp051Co4lwwPwO21NFIXWgjQmCPEgRTqQpei7qQaxlLGkrIPjZtJQRgCuC+Sg8HFw1YpUaMybN0nFInInQLp/qe0yc9ByDZM2G86NX6W5W3+j87I8Fh1dnMov1iJ0DFVn8RLwdEGjreiZCRyJOMuHghh6y4EG7W8BwmZrse17zhSpc2wFOVhxeZnYAQFEw6g48LutFRDpoTjGgz1nz/L4zcaUxxigs8wdY+qTTOHxSTxlLqwSZPFLyYrV2KJ9mKahMuYUy6o2b8snsjvnSjyK0kY+U0C6c8fmPDFUc0RqJqfdnsIUyh11U8d3NZdaFWg0UW0SNK3"; + // when + instance.decodePublicKey(rsaStr); + } + + @Test + public void authorizedKeysDecoderValidDSAKey() throws GeneralSecurityException { + // given + String dsaStr = "AAAAB3NzaC1kc3MAAACBANkM1e45lxlyV24QyWBAoESlHzhYYJUfk/yUd0+Dv28okyO71DmnJesYyUzsKDpnFLlnFhxTTUGSg90fdrdubLFkRTGnHhweegMCf6kU1xyE3U6bpyMdiOXH7fOS6Q2B+qtaQRB4R5TEhdoJX648Ng+YZvLwdbZh3r/et4P46b3DAAAAFQDcu6qp67XRpzMoOS2fIL+VOxvmDwAAAIAeT3d/hbvzPoL8wV52gPtWJMU2EGoX/LJwc86Vn52NlxXB1EQSzZI50PgCKEckS80lj4GXO1ZyuBhdsBEz4rDtAIdZGW5z7WxTfcz0G2dOWmNOBqvu7j9ngfPrgtDVHYV2VL/4VpbmoPgkQLfbA9NWb6US2RnTO46rGbGurigDMQAAAIEAiI3REuOJAmgDow6HxbN0FM+RCe1JYDwJIsCRRK4JA9oYV4Pg897xqypOeXogutVu9usfcOJI6uk5OwwLqIUSaU+flgmL0LOXv4lH4+URqs7Or8+ABFTcVGGCxg0I3gwhlY2Vjc9nyHY15wqBYdUxLbe8HC6EQp9uwlLlb8LQ6a0="; + // when + PublicKey serverKey = instance.decodePublicKey(dsaStr); + // then + assertEquals(serverKey.getAlgorithm(), "DSA"); + } + + @Test(expected = IllegalArgumentException.class) + public void authorizedKeysDecoderInvalidDSAKey() throws GeneralSecurityException { + // given + String dsaStr = "AAAAB3Nzakc3MAAACBANkM1e45lxlyV24QyWBAoESlHzhYYJUfk/yUd0+Dv28okyO71DmnJesYyUzsKDpnFLlnFhxTTUGSg90fdrdubLFkRTGnHhweegMCf6kU1xyE3U6bpyMdiOXH7fOS6Q2B+qtaQRB4R5TEhdoJX648Ng+YZvLwdbZh3r/et4P46b3DAAAAFQDcu6qp67XRpzMoOS2fIL+VOxvmDwAAAIAeT3d/hbvzPoL8wV52gPtWJMU2EGoX/LJwc86Vn52NlxXB1EQSzZI50PgCKEckS80lj4GXO1ZyuBhdsBEz4rDtAIdZGW5z7WxTfcz0G2dOWmNOBqvu7j9ngfPrgtDVHYV2VL/4VpbmoPgkQLfbA9NWb6US2RnTO46rGbGurigDMQAAAIEAiI3REuOJAmgDow6HxbN0FM+RCe1JYDwJIsCRRK4JA9oYV4Pg897xqypOeXogutVu9usfcOJI6uk5OwwLqIUSaU+flgmL0LOXv4lH4+URqs7Or8+ABFTcVGGCxg0I3gwhlY2Vjc9nyHY15wqBYdUxLbe8HC6EQp9uwlLlb8LQ6a0="; + // when + instance.decodePublicKey(dsaStr); + } + + @Test + public void authorizedKeysDecoderValidECDSAKey() throws GeneralSecurityException { + // given + String ecdsaStr = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAP4dTrlwZmz8bZ1f901qWuFk7YelrL2WJG0jrCEAPo9UNM1wywpqjbaYUfoq+cevhLZaukDQ4N2Evux+YQ2zz0="; + // when + PublicKey serverKey = instance.decodePublicKey(ecdsaStr); + // then + assertEquals(serverKey.getAlgorithm(), "EC"); + } + + @Test(expected = IllegalArgumentException.class) + public void authorizedKeysDecoderInvalidECDSAKey() throws GeneralSecurityException { + // given + String ecdsaStr = "AAAAE2VjZHNhLXNoItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAP4dTrlwZmz8bZ1f901qWuFk7YelrL2WJG0jrCEAPo9UNM1wywpqjbaYUfoq+cevhLZaukDQ4N2Evux+YQ2zz0="; + // when + instance.decodePublicKey(ecdsaStr); + } + + @Test(expected = IllegalArgumentException.class) + public void authorizedKeysDecoderInvalidKeyType() throws GeneralSecurityException { + // given + String ed25519Str = "AAAAC3NzaC1lZDI1NTE5AAAAICIvyX9C+u3KZmJ8x4DuqJg1iAKOPObCgkX9plrvu29R"; + // when + instance.decodePublicKey(ed25519Str); + } + + @Test(expected = IllegalArgumentException.class) + public void decodingOfBlankInputIsCaughtAsAnError() throws GeneralSecurityException { + // when + instance.decodePublicKey(""); + } +} diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationTest.java new file mode 100644 index 0000000000..13c5b97e4b --- /dev/null +++ b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationTest.java @@ -0,0 +1,82 @@ +/* + * 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.protocol; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.*; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import org.apache.sshd.ClientSession; +import org.apache.sshd.client.session.ClientSessionImpl; +import org.junit.Ignore; +import org.junit.Test; + + +public class CallHomeAuthorizationTest +{ + @Test + public void anAuthorizationOfRejectedIsNotAllowed() { + // given + CallHomeAuthorization auth = CallHomeAuthorization.rejected(); + // expect + assertFalse(auth.isServerAllowed()); + } + + @Test(expected = IllegalStateException.class) + public void anAuthorizationOfRejectedCannotBeAppliedToASession() { + // given + CallHomeAuthorization auth = CallHomeAuthorization.rejected(); + // when + auth.applyTo(mock(ClientSession.class)); + } + + @Test + public void anAuthorizationOfAcceptanceIsAllowed() { + // given + String session = "some-session"; + String user = "some-user-name"; + ClientSessionImpl mockSession = mock(ClientSessionImpl.class); + doNothing().when(mockSession).setUsername(user); + + // and + CallHomeAuthorization auth = CallHomeAuthorization.serverAccepted(session, user).build(); + // when + auth.applyTo(mockSession); + // then + assertTrue(auth.isServerAllowed()); + } + + @Test + public void anAuthorizationOfAcceptanceCanBeAppliedToASession() { + // given + String session = "some-session"; + String user = "some-user-name"; + String pwd = "pwd1"; + KeyPair pair = new KeyPair(mock(PublicKey.class), mock(PrivateKey.class)); + ClientSessionImpl mockSession = mock(ClientSessionImpl.class); + doNothing().when(mockSession).setUsername(user); + doNothing().when(mockSession).addPasswordIdentity(pwd); + doNothing().when(mockSession).addPublicKeyIdentity(pair); + // and + CallHomeAuthorization auth = CallHomeAuthorization.serverAccepted(session, user) + .addPassword(pwd) + .addClientKeys(pair) + .build(); + // when + auth.applyTo(mockSession); + // then + verify(mockSession, times(1)).addPasswordIdentity(anyString()); + verify(mockSession, times(1)).addPublicKeyIdentity(any(KeyPair.class)); + } +} diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContextTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContextTest.java new file mode 100644 index 0000000000..3f2f468fc5 --- /dev/null +++ b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContextTest.java @@ -0,0 +1,194 @@ +/* + * 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.protocol; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.security.PublicKey; +import org.apache.sshd.ClientChannel; +import org.apache.sshd.ClientChannel.Streaming; +import org.apache.sshd.ClientSession; +import org.apache.sshd.client.channel.ChannelSubsystem; +import org.apache.sshd.client.future.OpenFuture; +import org.apache.sshd.client.session.ClientSessionImpl; +import org.apache.sshd.common.KeyExchange; +import org.apache.sshd.common.Session.AttributeKey; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoInputStream; +import org.apache.sshd.common.io.IoOutputStream; +import org.apache.sshd.common.io.IoReadFuture; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.util.Buffer; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.Mockito; +import org.opendaylight.netconf.client.NetconfClientSessionListener; +import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory; + +public class CallHomeSessionContextTest { + private ClientSessionImpl mockSession; + private CallHomeAuthorization mockAuth; + private ClientChannel mockChannel; + private InetSocketAddress address; + + private ReverseSshChannelInitializer mockChannelInitializer; + private CallHomeNetconfSubsystemListener subListener; + private EventLoopGroup mockNettyGroup; + private CallHomeSessionContext.Factory realFactory; + private CallHomeSessionContext instance; + private NetconfClientSessionNegotiatorFactory mockNegotiatior; + + @Before + public void setup() { + mockSession = mock(ClientSessionImpl.class); + mockAuth = mock(CallHomeAuthorization.class); + mockChannel = mock(ClientChannel.class); + address = mock(InetSocketAddress.class); + + mockNegotiatior = mock(NetconfClientSessionNegotiatorFactory.class); + subListener = mock(CallHomeNetconfSubsystemListener.class); + mockNettyGroup = mock(EventLoopGroup.class); + + realFactory = new CallHomeSessionContext.Factory(mockNettyGroup, mockNegotiatior, subListener); + + + + KeyExchange kexMock = Mockito.mock(KeyExchange.class); + Mockito.doReturn(kexMock).when(mockSession).getKex(); + + PublicKey keyMock = Mockito.mock(PublicKey.class); + Mockito.doReturn(keyMock).when(kexMock).getServerKey(); + IoReadFuture mockFuture = mock(IoReadFuture.class); + IoInputStream mockIn = mock(IoInputStream.class); + Mockito.doReturn(mockFuture).when(mockIn).read(any(Buffer.class)); + IoOutputStream mockOut = mock(IoOutputStream.class); + + Mockito.doReturn(mockIn).when(mockChannel).getAsyncOut(); + Mockito.doReturn(mockOut).when(mockChannel).getAsyncIn(); + + Mockito.doReturn(true).when(mockAuth).isServerAllowed(); + + IoSession ioSession = mock(IoSession.class); + Mockito.doReturn(ioSession).when(mockSession).getIoSession(); + Mockito.doReturn(address).when(ioSession).getRemoteAddress(); + Mockito.doReturn(null).when(mockSession).setAttribute(any(AttributeKey.class), any()); + Mockito.doReturn(null).when(mockSession).getAttribute(any(AttributeKey.class)); + Mockito.doReturn("testSession").when(mockSession).toString(); + + Mockito.doNothing().when(mockAuth).applyTo(mockSession); + Mockito.doReturn("test").when(mockAuth).getSessionName(); + } + + @Test + public void theContextShouldBeSettableAndRetrievableAsASessionAttribute() { + // redo instance below because previous constructor happened too early to capture behavior + instance = realFactory.createIfNotExists(mockSession, mockAuth, address); + // when + CallHomeSessionContext.getFrom(mockSession); + // then + verify(mockSession, times(1)).setAttribute(CallHomeSessionContext.SESSION_KEY, instance); + verify(mockSession, times(1)).getAttribute(CallHomeSessionContext.SESSION_KEY); + } + + @Test + public void anAuthorizeActionShouldApplyToTheBoundSession() throws IOException { + instance = realFactory.createIfNotExists(mockSession, mockAuth, address); + // when + Mockito.doReturn(null).when(mockSession).auth(); + instance.authorize(); + // then + verify(mockAuth, times(1)).applyTo(mockSession); + } + + @Test + public void creatingAChannelSuccessfullyShouldResultInAnAttachedListener() throws IOException { + // given + OpenFuture mockFuture = mock(OpenFuture.class); + ChannelSubsystem mockChannel = mock(ChannelSubsystem.class); + Mockito.doReturn(mockFuture).when(mockChannel).open(); + Mockito.doReturn(mockChannel).when(mockSession).createSubsystemChannel(anyString()); + + Mockito.doReturn(null).when(mockFuture).addListener(any(SshFutureListener.class)); + Mockito.doNothing().when(mockChannel).setStreaming(any(Streaming.class)); + instance = realFactory.createIfNotExists(mockSession, mockAuth, address); + // when + instance.openNetconfChannel(); + // then + verify(mockFuture, times(1)).addListener(any(SshFutureListener.class)); + } + + static class TestableContext extends CallHomeSessionContext { + MinaSshNettyChannel minaMock; + + public TestableContext(ClientSession sshSession, CallHomeAuthorization authorization, InetSocketAddress address, + CallHomeSessionContext.Factory factory, MinaSshNettyChannel minaMock) { + super(sshSession, authorization, address, factory); + this.minaMock = minaMock; + } + + @Override + protected MinaSshNettyChannel newMinaSshNettyChannel(ClientChannel netconfChannel) { + return minaMock; + } + } + + @Ignore + @Test + public void openingTheChannelSuccessfullyShouldFireActiveChannel() { + // given + MinaSshNettyChannel mockMinaChannel = mock(MinaSshNettyChannel.class); + CallHomeSessionContext.Factory mockFactory = mock(CallHomeSessionContext.Factory.class); + + ChannelFuture mockChanFuture = mock(ChannelFuture.class); + Mockito.doReturn(mockChanFuture).when(mockNettyGroup).register(any(Channel.class)); + + Mockito.doReturn(mockNettyGroup).when(mockFactory).getNettyGroup(); + Mockito.doReturn(mockChannelInitializer).when(mockFactory) + .getChannelInitializer(any(NetconfClientSessionListener.class)); + + ChannelPipeline mockPipeline = mock(ChannelPipeline.class); + Mockito.doReturn(mockPipeline).when(mockMinaChannel).pipeline(); + + OpenFuture mockFuture = mock(OpenFuture.class); + Mockito.doReturn(true).when(mockFuture).isOpened(); + + instance = new TestableContext(mockSession, mockAuth, address, mockFactory, mockMinaChannel); + SshFutureListener listener = instance.newSshFutureListener(mockChannel); + // when + listener.operationComplete(mockFuture); + // then + verify(mockPipeline, times(1)).fireChannelActive(); + } + + @Test + @Ignore + public void failureToOpenTheChannelShouldCauseTheSessionToClose() { + // given + SshFutureListener listener = instance.newSshFutureListener(mockChannel); + OpenFuture mockFuture = mock(OpenFuture.class); + Mockito.doReturn(false).when(mockFuture).isOpened(); + Mockito.doReturn(new RuntimeException("test")).when(mockFuture).getException(); + // when + listener.operationComplete(mockFuture); + // then + verify(mockSession, times(1)).close(anyBoolean()); + } +} diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannelTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannelTest.java new file mode 100644 index 0000000000..17193a2382 --- /dev/null +++ b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannelTest.java @@ -0,0 +1,105 @@ +/* + * 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.protocol; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.EmptyByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import org.apache.sshd.ClientChannel; +import org.apache.sshd.ClientSession; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoInputStream; +import org.apache.sshd.common.io.IoOutputStream; +import org.apache.sshd.common.io.IoReadFuture; +import org.apache.sshd.common.io.IoWriteFuture; +import org.apache.sshd.common.util.Buffer; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.Mockito; + + +public class MinaSshNettyChannelTest { + CallHomeSessionContext mockContext; + ClientSession mockSession; + ClientChannel mockChannel; + MinaSshNettyChannel instance; + + @Before + public void setup() { + IoReadFuture mockFuture = mock(IoReadFuture.class); + IoInputStream mockIn = mock(IoInputStream.class); + Mockito.doReturn(mockFuture).when(mockIn).read(any(Buffer.class)); + IoOutputStream mockOut = mock(IoOutputStream.class); + mockContext = mock(CallHomeSessionContext.class); + mockSession = mock(ClientSession.class); + mockChannel = mock(ClientChannel.class); + Mockito.doReturn(mockIn).when(mockChannel).getAsyncOut(); + Mockito.doReturn(mockOut).when(mockChannel).getAsyncIn(); + + IoWriteFuture mockWrFuture = mock(IoWriteFuture.class); + Mockito.doReturn(false).when(mockOut).isClosed(); + Mockito.doReturn(false).when(mockOut).isClosing(); + Mockito.doReturn(mockWrFuture).when(mockOut).write(any(Buffer.class)); + Mockito.doReturn(null).when(mockWrFuture).addListener(any()); + + Mockito.doReturn(mockFuture).when(mockFuture).addListener(Mockito.any()); + + instance = new MinaSshNettyChannel(mockContext, mockSession, mockChannel); + } + + @Test + public void ourChannelHandlerShouldBeFirstInThePipeline() { + // given + ChannelHandler firstHandler = instance.pipeline().first(); + String firstName = firstHandler.getClass().getName(); + // expect + assertTrue(firstName.contains("callhome")); + } + + @Test + public void ourChannelHandlerShouldForwardWrites() throws Exception { + ChannelHandler mockHandler = mock(ChannelHandler.class); + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + Mockito.doReturn(mockHandler).when(ctx).handler(); + ChannelPromise promise = mock(ChannelPromise.class); + + ByteBufAllocator mockAlloc = mock(ByteBufAllocator.class); + ByteBuf bytes = new EmptyByteBuf(mockAlloc); + + // we would really like to just verify that the async handler write() was + // called but it is a final class, so no mocking. instead we set up the + // mock channel to have no async input, which will cause a failure later + // on the write promise that we use as a cheap way to tell that write() + // got called. ick. + + Mockito.doReturn(null).when(mockChannel).getAsyncIn(); + Mockito.doReturn(null).when(promise).setFailure(any(Throwable.class)); + + // Need to reconstruct instance to pick up null async in above + instance = new MinaSshNettyChannel(mockContext, mockSession, mockChannel); + + // when + ChannelOutboundHandlerAdapter outadapter = (ChannelOutboundHandlerAdapter) instance.pipeline().first(); + outadapter.write(ctx, bytes, promise); + + // then + verify(promise, times(1)).setFailure(any(Throwable.class)); + } +} diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerTest.java new file mode 100644 index 0000000000..2530217752 --- /dev/null +++ b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerTest.java @@ -0,0 +1,181 @@ +/* + * 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.protocol; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.PublicKey; +import java.util.HashMap; +import java.util.Map; +import org.apache.sshd.ClientSession; +import org.apache.sshd.SshClient; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.client.session.ClientSessionImpl; +import org.apache.sshd.common.Session; +import org.apache.sshd.common.SessionListener; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoAcceptor; +import org.apache.sshd.common.io.IoHandler; +import org.apache.sshd.common.io.IoServiceFactory; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NetconfCallHomeServerTest { + + SshClient mockSshClient; + CallHomeAuthorizationProvider mockCallHomeAuthProv; + CallHomeAuthorization mockAuth; + CallHomeSessionContext.Factory mockFactory; + InetSocketAddress mockAddress; + ClientSession mockSession; + + NetconfCallHomeServer instance; + + @Before + public void setup() { + mockSshClient = Mockito.spy(SshClient.setUpDefaultClient()); + mockCallHomeAuthProv = mock(CallHomeAuthorizationProvider.class); + mockAuth = mock(CallHomeAuthorization.class); + mockFactory = mock(CallHomeSessionContext.Factory.class); + mockAddress = InetSocketAddress.createUnresolved("1.2.3.4", 123); + mockSession = mock(ClientSession.class); + + Map props = new HashMap<>(); + props.put("nio-workers", "1"); + Mockito.doReturn(props).when(mockSshClient).getProperties(); + Mockito.doReturn("test").when(mockSession).toString(); + instance = new NetconfCallHomeServer(mockSshClient, mockCallHomeAuthProv, mockFactory, mockAddress); + } + + @Test + public void sessionListenerShouldHandleEventsOfKeyEstablishedAndAuthenticated () throws IOException { + // Weird - IJ was ok but command line compile failed using the usual array initializer syntax ???? + SessionListener.Event[] evt = new SessionListener.Event[2]; + evt[0] = SessionListener.Event.KeyEstablished; + evt[1] = SessionListener.Event.Authenticated; + + int[] hitOpen = new int[2]; + hitOpen[0] = 0; + hitOpen[1] = 1; + + int[] hitAuth = new int[2]; + hitAuth[0] = 1; + hitAuth[1] = 0; + + for (int pass = 0; pass < evt.length; pass++) + { + // given + AuthFuture mockAuthFuture = mock(AuthFuture.class); + Mockito.doReturn(null).when(mockAuthFuture).addListener(any(SshFutureListener.class)); + CallHomeSessionContext mockContext = mock(CallHomeSessionContext.class); + Mockito.doNothing().when(mockContext).openNetconfChannel(); + Mockito.doReturn(mockContext).when(mockSession).getAttribute(any(Session.AttributeKey.class)); + SessionListener listener = instance.createSessionListener(); + Mockito.doReturn(mockAuthFuture).when(mockContext).authorize(); + // when + listener.sessionEvent(mockSession, evt[pass]); + // then + verify(mockContext, times(hitOpen[pass])).openNetconfChannel(); + verify(mockContext, times(hitAuth[pass])).authorize(); + } + } + + @Test + public void verificationOfTheServerKeyShouldBeSuccessfulForServerIsAllowed () { + // given + + ClientSessionImpl mockClientSession = mock(ClientSessionImpl.class); + Mockito.doReturn("test").when(mockClientSession).toString(); + SocketAddress mockSocketAddr = mock(SocketAddress.class); + Mockito.doReturn("testAddr").when(mockSocketAddr).toString(); + PublicKey mockPublicKey = mock(PublicKey.class); + + CallHomeAuthorization mockAuth = mock(CallHomeAuthorization.class); + Mockito.doReturn("test").when(mockAuth).toString(); + Mockito.doReturn(true).when(mockAuth).isServerAllowed(); + Mockito.doReturn("some-session-name").when(mockAuth).getSessionName(); + + Mockito.doReturn(mockAuth).when(mockCallHomeAuthProv).provideAuth(mockSocketAddr,mockPublicKey); + + Mockito.doReturn(null).when(mockFactory).createIfNotExists(mockClientSession, mockAuth, mockSocketAddr); + + // expect + instance.verifyServerKey(mockClientSession, mockSocketAddr, mockPublicKey); + } + + @Test + public void verificationOfTheServerKeyShouldFailIfTheServerIsNotAllowed () { + // given + + ClientSessionImpl mockClientSession = mock(ClientSessionImpl.class); + SocketAddress mockSocketAddr = mock(SocketAddress.class); + PublicKey mockPublicKey = mock(PublicKey.class); + + Mockito.doReturn(false).when(mockAuth).isServerAllowed(); + Mockito.doReturn(mockAuth).when(mockCallHomeAuthProv).provideAuth(mockSocketAddr, mockPublicKey); + Mockito.doReturn("").when(mockClientSession).toString(); + + // expect + assertFalse(instance.verifyServerKey(mockClientSession, mockSocketAddr, mockPublicKey)); + } + + static class TestableCallHomeServer extends NetconfCallHomeServer + { + static IoServiceFactory minaServiceFactory; + static SshClient factoryHook (SshClient client, IoServiceFactory minaFactory) + { + minaServiceFactory = minaFactory; + return client; + } + + SshClient client; + + TestableCallHomeServer(SshClient sshClient, CallHomeAuthorizationProvider authProvider, + CallHomeSessionContext.Factory factory, InetSocketAddress socketAddress, + IoServiceFactory minaFactory) { + super(factoryHook(sshClient, minaFactory), authProvider, factory, socketAddress); + client = sshClient; + } + + @Override + protected IoServiceFactory createMinaServiceFactory(SshClient sshClient) + { + return minaServiceFactory; + } + } + + @Test + public void bindShouldStartTheClientAndBindTheAddress () throws IOException { + // given + IoAcceptor mockAcceptor = mock(IoAcceptor.class); + IoServiceFactory mockMinaFactory = mock(IoServiceFactory.class); + + Mockito.doReturn(mockAcceptor).when(mockMinaFactory).createAcceptor(any(IoHandler.class)); + Mockito.doReturn(mockAcceptor).when(mockMinaFactory).createAcceptor(any(IoHandler.class)); + Mockito.doNothing().when(mockAcceptor).bind(mockAddress); + instance = new TestableCallHomeServer(mockSshClient, mockCallHomeAuthProv, mockFactory, mockAddress, mockMinaFactory); + // when + instance.bind(); + // then + verify(mockSshClient, times(1)).start(); + verify(mockAcceptor, times(1)).bind(mockAddress); + } + +} diff --git a/netconf/callhome-provider/pom.xml b/netconf/callhome-provider/pom.xml new file mode 100644 index 0000000000..f2a4ef91be --- /dev/null +++ b/netconf/callhome-provider/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + org.opendaylight.mdsal + binding-parent + 0.10.0-SNAPSHOT + + + + org.opendaylight.netconf + callhome-provider + 1.2.0-SNAPSHOT + bundle + + + + + org.opendaylight.netconf + netconf-subsystem + ${project.version} + pom + import + + + + + + + org.opendaylight.controller + sal-binding-api + + + org.opendaylight.controller + sal-binding-broker-impl + test-jar + test + + + org.opendaylight.netconf + netconf-topology + + + org.opendaylight.netconf + callhome-protocol + ${project.version} + + + org.opendaylight.netconf + callhome-model + ${project.version} + + + org.mockito + mockito-core + + + diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/BaseCallHomeTopology.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/BaseCallHomeTopology.java new file mode 100644 index 0000000000..b7861a30bc --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/BaseCallHomeTopology.java @@ -0,0 +1,35 @@ +/* + * 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 io.netty.util.concurrent.EventExecutor; +import org.opendaylight.controller.config.threadpool.ScheduledThreadPool; +import org.opendaylight.controller.config.threadpool.ThreadPool; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService; +import org.opendaylight.controller.sal.binding.api.BindingAwareBroker; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.netconf.client.NetconfClientDispatcher; +import org.opendaylight.netconf.topology.AbstractNetconfTopology; +import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider; + +abstract class BaseCallHomeTopology extends AbstractNetconfTopology { + + protected DOMMountPointService mountPointService = null; + + protected BaseCallHomeTopology(String topologyId, NetconfClientDispatcher clientDispatcher, + BindingAwareBroker bindingAwareBroker, Broker domBroker, EventExecutor eventExecutor, + ScheduledThreadPool keepaliveExecutor, ThreadPool processingExecutor, + SchemaRepositoryProvider schemaRepositoryProvider, DataBroker dataBroker, DOMMountPointService mountPointService) { + super(topologyId, clientDispatcher, bindingAwareBroker, domBroker, eventExecutor, keepaliveExecutor, + processingExecutor, schemaRepositoryProvider, dataBroker); + this.mountPointService = mountPointService; + } + +} diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeAuthProviderImpl.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeAuthProviderImpl.java new file mode 100644 index 0000000000..485df12d8a --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeAuthProviderImpl.java @@ -0,0 +1,190 @@ +/* + * 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.base.Objects; +import com.google.common.net.InetAddresses; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.PublicKey; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.binding.api.DataObjectModification; +import org.opendaylight.controller.md.sal.binding.api.DataTreeChangeListener; +import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier; +import org.opendaylight.controller.md.sal.binding.api.DataTreeModification; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.netconf.callhome.protocol.AuthorizedKeysDecoder; +import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorization; +import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorization.Builder; +import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorizationProvider; +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.credentials.Credentials; +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.Global; +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.yangtools.concepts.ListenerRegistration; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CallHomeAuthProviderImpl implements CallHomeAuthorizationProvider, AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(CallHomeAuthProviderImpl.class); + private static final InstanceIdentifier GLOBAL_PATH = + InstanceIdentifier.create(NetconfCallhomeServer.class).child(Global.class); + private static final DataTreeIdentifier GLOBAL = + new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION, GLOBAL_PATH); + + private static final InstanceIdentifier ALLOWED_DEVICES_PATH = + InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class); + private static final DataTreeIdentifier ALLOWED_DEVICES = + new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION, ALLOWED_DEVICES_PATH); + + private final GlobalConfig globalConfig = new GlobalConfig(); + private final DeviceConfig deviceConfig = new DeviceConfig(); + private final ListenerRegistration configReg; + private final ListenerRegistration deviceReg; + + public CallHomeAuthProviderImpl(DataBroker broker) { + configReg = broker.registerDataTreeChangeListener(GLOBAL, globalConfig); + deviceReg = broker.registerDataTreeChangeListener(ALLOWED_DEVICES, deviceConfig); + } + + @Override + public CallHomeAuthorization provideAuth(SocketAddress remoteAddress, PublicKey serverKey) { + Device deviceSpecific = deviceConfig.get(serverKey); + String sessionName; + Credentials deviceCred; + if (deviceSpecific != null) { + sessionName = deviceSpecific.getUniqueId(); + deviceCred = deviceSpecific.getCredentials(); + } else if (globalConfig.allowedUnknownKeys()) { + sessionName = fromRemoteAddress(remoteAddress); + deviceCred = null; + } else { + return CallHomeAuthorization.rejected(); + } + final Credentials credentials = deviceCred != null ? deviceCred : globalConfig.getCredentials(); + + if (credentials == null) { + LOG.info("No credentials found for {}, rejecting.", remoteAddress); + return CallHomeAuthorization.rejected(); + } + Builder authBuilder = CallHomeAuthorization.serverAccepted(sessionName, credentials.getUsername()); + for (String password : credentials.getPasswords()) { + authBuilder.addPassword(password); + } + return authBuilder.build(); + } + + @Override + public void close() throws Exception { + configReg.close(); + deviceReg.close(); + } + + private String fromRemoteAddress(SocketAddress remoteAddress) { + if (remoteAddress instanceof InetSocketAddress) { + InetSocketAddress socketAddress = (InetSocketAddress) remoteAddress; + return InetAddresses.toAddrString(socketAddress.getAddress()) + ":" + socketAddress.getPort(); + } + return remoteAddress.toString(); + } + + + private class DeviceConfig implements DataTreeChangeListener { + + private ConcurrentMap byPublicKey = new ConcurrentHashMap(); + + @Override + public void onDataTreeChanged(Collection> arg0) { + for (DataTreeModification dataTreeModification : arg0) { + DataObjectModification rootNode = dataTreeModification.getRootNode(); + process(rootNode); + } + } + + private void process(DataObjectModification deviceMod) { + Device before = deviceMod.getDataBefore(); + Device after = deviceMod.getDataAfter(); + + if (before == null) { + putDevice(after); + } else if (after == null) { + // Delete + removeDevice(before); + } else { + if (!Objects.equal(before.getSshHostKey(), after.getSshHostKey())) { + // key changed // we should remove previous key. + removeDevice(before); + } + putDevice(after); + } + } + + private void putDevice(Device device) { + PublicKey key = publicKey(device); + if (key == null) { + return; + } + byPublicKey.put(key, device); + } + + private void removeDevice(Device device) { + PublicKey key = publicKey(device); + if (key == null) { + return; + } + byPublicKey.remove(key); + } + + private PublicKey publicKey(Device device) { + String hostKey = device.getSshHostKey(); + try { + return new AuthorizedKeysDecoder().decodePublicKey(hostKey); + } catch (Exception e) { + LOG.error("Unable to decode SSH key for {}. Ignoring update for this device",device.getUniqueId(),e); + return null; + } + } + + private Device get(PublicKey key) { + return byPublicKey.get(key); + } + } + + private class GlobalConfig implements DataTreeChangeListener { + + + private volatile Global current = null; + + @Override + public void onDataTreeChanged(Collection> arg0) { + for (DataTreeModification dataTreeModification : arg0) { + current = dataTreeModification.getRootNode().getDataAfter(); + } + } + + boolean allowedUnknownKeys() { + if (current == null) { + return false; + } + // Deal with null values. + return Boolean.TRUE.equals(current.isAcceptAllSshKeys()); + } + + Credentials getCredentials() { + return current != null ? current.getCredentials() : null; + } + + } + +} diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcher.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcher.java new file mode 100644 index 0000000000..807b7a1661 --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcher.java @@ -0,0 +1,117 @@ +/* + * 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 io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.FailedFuture; +import io.netty.util.concurrent.Future; +import java.net.InetSocketAddress; +import org.opendaylight.controller.config.threadpool.ScheduledThreadPool; +import org.opendaylight.controller.config.threadpool.ThreadPool; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService; +import org.opendaylight.controller.sal.binding.api.BindingAwareBroker; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.netconf.callhome.mount.CallHomeMountSessionContext.CloseCallback; +import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator; +import org.opendaylight.netconf.callhome.protocol.CallHomeNetconfSubsystemListener; +import org.opendaylight.netconf.callhome.protocol.CallHomeProtocolSessionContext; +import org.opendaylight.netconf.client.NetconfClientDispatcher; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.netconf.client.conf.NetconfClientConfiguration; +import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfiguration; +import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider; +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.network.topology.topology.Node; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class CallHomeMountDispatcher implements NetconfClientDispatcher, CallHomeNetconfSubsystemListener { + + private final static Logger LOG = LoggerFactory.getLogger(CallHomeMountDispatcher.class); + + private final String topologyId; + private final BindingAwareBroker bindingAwareBroker; + private final EventExecutor eventExecutor; + private final ScheduledThreadPool keepaliveExecutor; + private final ThreadPool processingExecutor; + private final SchemaRepositoryProvider schemaRepositoryProvider; + private final org.opendaylight.controller.sal.core.api.Broker domBroker; + private final CallHomeMountSessionManager sessionManager; + private final DataBroker dataBroker; + private final DOMMountPointService mountService; + + + private CallHomeTopology topology; + + private final CloseCallback onCloseHandler = new CloseCallback() { + + @Override + public void onClosed(CallHomeMountSessionContext deviceContext) { + LOG.info("Removing {} from Netconf Topology.", deviceContext.getId()); + topology.disconnectNode(deviceContext.getId()); + } + }; + + + public CallHomeMountDispatcher(String topologyId, BindingAwareBroker bindingAwareBroker, + EventExecutor eventExecutor, ScheduledThreadPool keepaliveExecutor, ThreadPool processingExecutor, + SchemaRepositoryProvider schemaRepositoryProvider, Broker domBroker, DataBroker dataBroker, DOMMountPointService mountService) { + this.topologyId = topologyId; + this.bindingAwareBroker = bindingAwareBroker; + this.eventExecutor = eventExecutor; + this.keepaliveExecutor = keepaliveExecutor; + this.processingExecutor = processingExecutor; + this.schemaRepositoryProvider = schemaRepositoryProvider; + this.domBroker = domBroker; + this.sessionManager = new CallHomeMountSessionManager(); + this.dataBroker = dataBroker; + this.mountService = mountService; + } + + + @Override + public Future createClient(NetconfClientConfiguration clientConfiguration) { + return activateChannel(clientConfiguration); + } + + @Override + public Future createReconnectingClient(NetconfReconnectingClientConfiguration clientConfiguration) { + return activateChannel(clientConfiguration); + } + + private Future activateChannel(NetconfClientConfiguration conf) { + InetSocketAddress remoteAddr = conf.getAddress(); + CallHomeMountSessionContext context = sessionManager.getByAddress(remoteAddr); + LOG.info("Activating NETCONF channel for ip {} device context {}", remoteAddr, context); + if (context == null) { + return new FailedFuture<>(eventExecutor, new NullPointerException()); + } + return context.activateNetconfChannel(conf.getSessionListener()); + } + + void createTopology() { + this.topology = new CallHomeTopology(topologyId, this, bindingAwareBroker, domBroker, eventExecutor, + keepaliveExecutor, processingExecutor, schemaRepositoryProvider, dataBroker, mountService); + } + + @Override + public void onNetconfSubsystemOpened(CallHomeProtocolSessionContext session, CallHomeChannelActivator activator) { + CallHomeMountSessionContext deviceContext = sessionManager.createSession(session, activator, onCloseHandler); + NodeId nodeId = deviceContext.getId(); + Node configNode = deviceContext.getConfigNode(); + LOG.info("Provisioning fake config {}", configNode); + topology.connectNode(nodeId, configNode); + } + + public CallHomeMountSessionManager getSessionManager() { + return sessionManager; + } +} diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContext.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContext.java new file mode 100644 index 0000000000..42d6aab149 --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContext.java @@ -0,0 +1,130 @@ +/* + * 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.base.Preconditions; +import io.netty.util.concurrent.Promise; +import java.net.InetSocketAddress; +import java.security.PublicKey; +import org.opendaylight.netconf.api.NetconfMessage; +import org.opendaylight.netconf.api.NetconfTerminationReason; +import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator; +import org.opendaylight.netconf.callhome.protocol.CallHomeProtocolSessionContext; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.netconf.client.NetconfClientSessionListener; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host; +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.NetconfNodeBuilder; +import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.netconf.node.credentials.credentials.LoginPasswordBuilder; +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.network.topology.topology.Node; +import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeBuilder; + +class CallHomeMountSessionContext { + + public interface CloseCallback { + + void onClosed(CallHomeMountSessionContext deviceContext); + + } + + private final NodeId nodeId; + private final CallHomeChannelActivator activator; + private final CallHomeProtocolSessionContext protocol; + private final CloseCallback onClose; + // FIXME: Remove this + private final ContextKey key; + + CallHomeMountSessionContext(String nodeId, CallHomeProtocolSessionContext protocol, + CallHomeChannelActivator activator, CloseCallback callback) { + + this.nodeId = new NodeId(Preconditions.checkNotNull(nodeId, "nodeId")); + this.key = ContextKey.from(protocol.getRemoteAddress()); + this.protocol = Preconditions.checkNotNull(protocol, "protocol"); + this.activator = Preconditions.checkNotNull(activator, "activator"); + this.onClose = Preconditions.checkNotNull(callback, "callback"); + } + + NodeId getId() { + return nodeId; + } + + public ContextKey getContextKey() { + return key; + } + + Node getConfigNode() { + NodeBuilder builder = new NodeBuilder(); + + return builder.setNodeId(getId()).addAugmentation(NetconfNode.class, configNetconfNode()).build(); + + } + + private NetconfNode configNetconfNode() { + NetconfNodeBuilder node = new NetconfNodeBuilder(); + node.setHost(new Host(key.getIpAddress())); + node.setPort(key.getPort()); + node.setTcpOnly(Boolean.FALSE); + node.setCredentials(new LoginPasswordBuilder().setUsername("ommited").setPassword("ommited").build()); + node.setSchemaless(Boolean.FALSE); + return node.build(); + } + + @SuppressWarnings("unchecked") + Promise activateNetconfChannel(NetconfClientSessionListener sessionListener) { + return (Promise) activator.activate(wrap(sessionListener)); + } + + @SuppressWarnings("deprecation") + private NetconfClientSessionListener wrap(final NetconfClientSessionListener delegate) { + return new NetconfClientSessionListener() { + + @Override + public void onSessionUp(NetconfClientSession session) { + delegate.onSessionUp(session); + } + + @Override + public void onSessionTerminated(NetconfClientSession session, NetconfTerminationReason reason) { + try { + delegate.onSessionTerminated(session, reason); + } finally { + removeSelf(); + } + } + + @Override + public void onSessionDown(NetconfClientSession session, Exception e) { + try { + removeSelf(); + } finally { + delegate.onSessionDown(session, e); + } + } + + @Override + public void onMessage(NetconfClientSession session, NetconfMessage message) { + delegate.onMessage(session, message); + } + }; + } + + private void removeSelf() { + onClose.onClosed(this); + } + + InetSocketAddress getRemoteAddress() { + return protocol.getRemoteAddress(); + } + + PublicKey getRemoteServerKey() { + return protocol.getRemoteServerKey(); + } + +} diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionManager.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionManager.java new file mode 100644 index 0000000000..7f6187d5e2 --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionManager.java @@ -0,0 +1,60 @@ +/* + * 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.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.PublicKey; +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.annotation.Nullable; +import org.opendaylight.netconf.callhome.mount.CallHomeMountSessionContext.CloseCallback; +import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator; +import org.opendaylight.netconf.callhome.protocol.CallHomeProtocolSessionContext; + +public class CallHomeMountSessionManager implements CallHomeMountSessionContext.CloseCallback { + + private final ConcurrentMap contextByAddress = new ConcurrentHashMap<>(); + private final Multimap contextByPublicKey = MultimapBuilder.hashKeys().hashSetValues().build(); + + @Nullable + public CallHomeMountSessionContext getByAddress(InetSocketAddress remoteAddr) { + return contextByAddress.get(remoteAddr); + } + + @Nullable + public Collection getByPublicKey(PublicKey publicKey) { + return contextByPublicKey.get(publicKey); + } + + CallHomeMountSessionContext createSession(CallHomeProtocolSessionContext session, + CallHomeChannelActivator activator, final CloseCallback onCloseHandler) { + + String name = session.getSessionName(); + CallHomeMountSessionContext deviceContext = new CallHomeMountSessionContext(name, session, activator, devCtxt -> { + CallHomeMountSessionManager.this.onClosed(devCtxt); + onCloseHandler.onClosed(devCtxt); + }); + + contextByAddress.put(deviceContext.getRemoteAddress(), deviceContext); + contextByPublicKey.put(deviceContext.getRemoteServerKey(), deviceContext); + + return deviceContext; + } + + @Override + public synchronized void onClosed(CallHomeMountSessionContext deviceContext) { + contextByAddress.remove(deviceContext.getRemoteAddress()); + contextByPublicKey.remove(deviceContext.getRemoteServerKey(),deviceContext); + } + +} diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeTopology.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeTopology.java new file mode 100644 index 0000000000..bdacf1d87d --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeTopology.java @@ -0,0 +1,41 @@ +/* + * 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 io.netty.util.concurrent.EventExecutor; +import org.opendaylight.controller.config.threadpool.ScheduledThreadPool; +import org.opendaylight.controller.config.threadpool.ThreadPool; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService; +import org.opendaylight.controller.sal.binding.api.BindingAwareBroker; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.netconf.client.NetconfClientDispatcher; +import org.opendaylight.netconf.sal.connect.api.RemoteDeviceHandler; +import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfSessionPreferences; +import org.opendaylight.netconf.sal.connect.netconf.sal.NetconfDeviceSalFacade; +import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId; +import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider; + + +public class CallHomeTopology extends BaseCallHomeTopology { + + public CallHomeTopology(String topologyId, NetconfClientDispatcher clientDispatcher, + BindingAwareBroker bindingAwareBroker, Broker domBroker, EventExecutor eventExecutor, + ScheduledThreadPool keepaliveExecutor, ThreadPool processingExecutor, + SchemaRepositoryProvider schemaRepositoryProvider,final DataBroker dataBroker, final DOMMountPointService mountPointService) { + super(topologyId, clientDispatcher, bindingAwareBroker, domBroker, eventExecutor, keepaliveExecutor, processingExecutor, + schemaRepositoryProvider, dataBroker, mountPointService); + } + + @Override + protected RemoteDeviceHandler createSalFacade(RemoteDeviceId id, Broker domBroker, + BindingAwareBroker bindingBroker) { + return new NetconfDeviceSalFacade(id, domBroker, bindingAwareBroker); + } +} diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/Configuration.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/Configuration.java new file mode 100644 index 0000000000..f115b86b6a --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/Configuration.java @@ -0,0 +1,112 @@ +/* + * 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 java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class Configuration { + public abstract static class ConfigurationException extends RuntimeException { + ConfigurationException(String msg) { + super(msg); + } + + ConfigurationException(String msg, Exception cause) { + super(msg, cause); + } + } + + public static class ReadException extends ConfigurationException { + ReadException(String msg, Exception e) { + super(msg, e); + } + } + + public static class MissingException extends ConfigurationException { + private final String key; + + MissingException(String key) { + super("Key not found: " + key); + this.key = key; + } + + public String getKey() { + return key; + } + } + + public static class IllegalValueException extends ConfigurationException { + private final String key; + private final String value; + + IllegalValueException(String key, String value) { + super("Key has an illegal value. Key: " + key + ", Value: " + value); + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + } + + private String path; + private Properties properties; + + public Configuration(String path) throws ConfigurationException { + this.path = path; + try { + this.properties = readFromPath(path); + } catch (IOException ioe) { + throw new ReadException(path, ioe); + } + } + + private Properties readFromPath(String path) throws IOException { + return readFromFile(new File(path)); + } + + private Properties readFromFile(File file) throws IOException { + FileInputStream stream = new FileInputStream(file); + properties = readFrom(stream); + return properties; + } + + private Properties readFrom(InputStream stream) throws IOException { + Properties properties = new Properties(); + properties.load(stream); + return properties; + } + + public String get(String key) { + String result = (String) properties.get(key); + if (result == null) + throw new MissingException(key); + return result; + } + + public int getAsPort(String key) { + String s = get(key); + try { + int newPort = Integer.parseInt(s); + if (newPort < 0 || newPort > 65535) + throw new IllegalValueException(key, s); + return newPort; + } catch (NumberFormatException e) { + throw new IllegalValueException(key, s); + } + } +} diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/ContextKey.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/ContextKey.java new file mode 100644 index 0000000000..cb0578fe3b --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/ContextKey.java @@ -0,0 +1,87 @@ +/* + * 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.base.Objects; +import com.google.common.base.Preconditions; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber; +import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode; + +class ContextKey { + + private final IpAddress address; + private final PortNumber port; + + public ContextKey(IpAddress address, PortNumber port) { + this.address = Preconditions.checkNotNull(address); + this.port = Preconditions.checkNotNull(port); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + address.hashCode(); + result = prime * result + port.hashCode(); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ContextKey other = (ContextKey) obj; + return Objects.equal(address, other.address) && Objects.equal(port, other.port); + } + + public static ContextKey from(NetconfNode node) { + return new ContextKey(node.getHost().getIpAddress(), node.getPort()); + } + + IpAddress getIpAddress() { + return address; + } + + PortNumber getPort() { + return port; + } + + public static ContextKey from(SocketAddress remoteAddress) { + Preconditions.checkArgument(remoteAddress instanceof InetSocketAddress); + InetSocketAddress inetSocketAddr = (InetSocketAddress) remoteAddress; + InetAddress ipAddress = inetSocketAddr.getAddress(); + + final IpAddress yangIp; + if(ipAddress instanceof Inet4Address) { + yangIp = new IpAddress(IetfInetUtil.INSTANCE.ipv4AddressFor(ipAddress)); + } else { + Preconditions.checkArgument(ipAddress instanceof Inet6Address); + yangIp = new IpAddress(IetfInetUtil.INSTANCE.ipv6AddressFor(ipAddress)); + } + return new ContextKey(yangIp, new PortNumber(inetSocketAddr.getPort())); + } + + @Override + public String toString() { + if(address.getIpv4Address() != null) { + return address.getIpv4Address().getValue() + ":" + port.getValue(); + } + return address.getIpv6Address().getValue() + ":" + port.getValue(); + } +} \ No newline at end of file diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/IetfZeroTouchCallHomeServerProvider.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/IetfZeroTouchCallHomeServerProvider.java new file mode 100644 index 0000000000..26373359e3 --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/IetfZeroTouchCallHomeServerProvider.java @@ -0,0 +1,98 @@ +/* + * 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 java.io.IOException; +import java.io.File; +import java.net.InetSocketAddress; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorizationProvider; +import org.opendaylight.netconf.callhome.protocol.NetconfCallHomeServer; +import org.opendaylight.netconf.callhome.protocol.NetconfCallHomeServerBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IetfZeroTouchCallHomeServerProvider implements AutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(IetfZeroTouchCallHomeServerProvider.class); + private static final String appName = "CallHomeServer"; + + private final DataBroker dataBroker; + private final CallHomeMountDispatcher mountDispacher; + + protected NetconfCallHomeServer server; + + + private static final String CALL_HOME_PORT_KEY = "DefaultCallHomePort"; + + private final CallHomeAuthProviderImpl authProvider; + + + static String configurationPath = "etc"+File.pathSeparator+"ztp-callhome-config.cfg"; + + private int port = 0; // 0 = use default in NetconfCallHomeBuilder + + public IetfZeroTouchCallHomeServerProvider(DataBroker dataBroker, CallHomeMountDispatcher mountDispacher) { + this.dataBroker = dataBroker; + this.mountDispacher = mountDispacher; + this.authProvider = new CallHomeAuthProviderImpl(dataBroker); + } + + public void init() { + // Register itself as a listener to changes in Devices subtree + try { + LOG.info("Initializing provider for {}", appName); + loadConfigurableValues(configurationPath); + initializeServer(); + LOG.info("Initialization complete for {}", appName); + } catch (IndexOutOfBoundsException | Configuration.ConfigurationException e) { + LOG.error("Unable to successfully initialize", e); + } + } + + void loadConfigurableValues(String configurationPath) throws Configuration.ConfigurationException { + try { + Configuration configuration = new Configuration(configurationPath); + port = configuration.getAsPort(CALL_HOME_PORT_KEY); + } catch (Exception e) { + LOG.error("Problem trying to load configuration values from {}", configurationPath, e); + } + } + + private CallHomeAuthorizationProvider getCallHomeAuthorization() { + return authProvider; + } + + private void initializeServer() { + LOG.info("Initializing Call Home server instance"); + CallHomeAuthorizationProvider auth = getCallHomeAuthorization(); + NetconfCallHomeServerBuilder builder = new NetconfCallHomeServerBuilder(auth, mountDispacher); + + if (port > 0) + builder.setBindAddress(new InetSocketAddress(port)); + server = builder.build(); + try { + server.bind(); + mountDispacher.createTopology(); + LOG.info("Initialization complete for Call Home server instance"); + } catch (IOException e) { + throw new IllegalStateException("Unable to create Call Home Server.",e); + } + } + + @Override + public void close() throws Exception { + authProvider.close(); + if (server != null) { + server.close(); + } + + LOG.info("Successfully closed provider for {}", appName); + } + +} diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/SchemaRepositoryProviderImpl.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/SchemaRepositoryProviderImpl.java new file mode 100644 index 0000000000..331ea85a3a --- /dev/null +++ b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/SchemaRepositoryProviderImpl.java @@ -0,0 +1,26 @@ +/* + * 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 org.opendaylight.netconf.topology.api.SchemaRepositoryProvider; +import org.opendaylight.yangtools.yang.parser.repo.SharedSchemaRepository; + +// FIXME: Figure out why blueprint rejects to instantiate if class is not public +public class SchemaRepositoryProviderImpl implements SchemaRepositoryProvider { + + private final SharedSchemaRepository schemaRepository; + + public SchemaRepositoryProviderImpl(final String moduleName) { + schemaRepository = new SharedSchemaRepository(moduleName); + } + + @Override + public SharedSchemaRepository getSharedSchemaRepository() { + return schemaRepository; + } +} \ No newline at end of file diff --git a/netconf/callhome-provider/src/main/resources/org/opendaylight/blueprint/callhome-topology.xml b/netconf/callhome-provider/src/main/resources/org/opendaylight/blueprint/callhome-topology.xml new file mode 100755 index 0000000000..657a4810e2 --- /dev/null +++ b/netconf/callhome-provider/src/main/resources/org/opendaylight/blueprint/callhome-topology.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcherTest.java b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcherTest.java new file mode 100644 index 0000000000..fc59e276a4 --- /dev/null +++ b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcherTest.java @@ -0,0 +1,117 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; + +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.controller.config.threadpool.ScheduledThreadPool; +import org.opendaylight.controller.config.threadpool.ThreadPool; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService; +import org.opendaylight.controller.sal.binding.api.BindingAwareBroker; +import org.opendaylight.controller.sal.core.api.Broker; +import org.opendaylight.netconf.api.messages.NetconfHelloMessageAdditionalHeader; +import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.netconf.client.NetconfClientSessionListener; +import org.opendaylight.netconf.client.conf.NetconfClientConfiguration; +import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder; +import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.AuthenticationHandler; +import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider; +import org.opendaylight.protocol.framework.ReconnectStrategy; + +public class CallHomeMountDispatcherTest { + private String topologyId; + private BindingAwareBroker mockBroker; + private EventExecutor mockExecutor; + private ScheduledThreadPool mockKeepAlive; + private ThreadPool mockProcessingExecutor; + private SchemaRepositoryProvider mockSchemaRepoProvider; + private Broker mockDomBroker; + + private CallHomeMountDispatcher instance; + private DataBroker mockDataBroker; + private DOMMountPointService mockMount; + + @Before + public void setup() { + topologyId = ""; + mockBroker = mock(BindingAwareBroker.class); + mockExecutor = mock(EventExecutor.class); + mockKeepAlive = mock(ScheduledThreadPool.class); + mockProcessingExecutor = mock(ThreadPool.class); + mockSchemaRepoProvider = mock(SchemaRepositoryProvider.class); + mockDomBroker = mock(Broker.class); + mockDataBroker = mock(DataBroker.class); + mockMount = mock(DOMMountPointService.class); + instance = new CallHomeMountDispatcher(topologyId, mockBroker, mockExecutor, mockKeepAlive, + mockProcessingExecutor, mockSchemaRepoProvider, mockDomBroker, mockDataBroker, mockMount); + } + + NetconfClientConfiguration someConfiguration(InetSocketAddress address) { + // NetconfClientConfiguration has mostly final methods, making it un-mock-able + + NetconfClientConfiguration.NetconfClientProtocol protocol = + NetconfClientConfiguration.NetconfClientProtocol.SSH; + NetconfHelloMessageAdditionalHeader additionalHeader = mock(NetconfHelloMessageAdditionalHeader.class); + NetconfClientSessionListener sessionListener = mock(NetconfClientSessionListener.class); + ReconnectStrategy reconnectStrategy = mock(ReconnectStrategy.class); + AuthenticationHandler authHandler = mock(AuthenticationHandler.class); + + return NetconfClientConfigurationBuilder.create().withProtocol(protocol).withAddress(address) + .withConnectionTimeoutMillis(0).withAdditionalHeader(additionalHeader) + .withSessionListener(sessionListener).withReconnectStrategy(reconnectStrategy) + .withAuthHandler(authHandler).build(); + } + + @Test + public void canCreateASessionFromAConfiguration() { + // given + CallHomeMountSessionContext mockContext = mock(CallHomeMountSessionContext.class); + InetSocketAddress someAddress = InetSocketAddress.createUnresolved("1.2.3.4", 123); + // instance.contextByAddress.put(someAddress, mockContext); + + NetconfClientConfiguration someCfg = someConfiguration(someAddress); + // when + instance.createClient(someCfg); + // then + // verify(mockContext, times(1)).activate(any(NetconfClientSessionListener.class)); + } + + @Test + public void noSessionIsCreatedWithoutAContextAvailableForAGivenAddress() { + // given + InetSocketAddress someAddress = InetSocketAddress.createUnresolved("1.2.3.4", 123); + NetconfClientConfiguration someCfg = someConfiguration(someAddress); + // when + Future future = instance.createClient(someCfg); + // then + assertFalse(future.isSuccess()); + } + + @Test + public void nodeIsInsertedIntoTopologyWhenSubsystemIsOpened() throws UnknownHostException { + // given + InetSocketAddress someAddress = new InetSocketAddress(InetAddress.getByName("1.2.3.4"), 123); + CallHomeChannelActivator activator = mock(CallHomeChannelActivator.class); + // instance.topology = mock(CallHomeTopology.class); + // when + // instance.onNetconfSubsystemOpened(someAddress, activator); + // then + // verify(instance.topology, times(1)).connectNode(any(NodeId.class), any(Node.class)); + } +} diff --git a/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContextTest.java b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContextTest.java new file mode 100644 index 0000000000..c91f7a8d0e --- /dev/null +++ b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContextTest.java @@ -0,0 +1,141 @@ +/* + * 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 static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.netty.util.concurrent.Promise; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.opendaylight.netconf.api.NetconfMessage; +import org.opendaylight.netconf.api.NetconfTerminationReason; +import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator; +import org.opendaylight.netconf.callhome.protocol.CallHomeProtocolSessionContext; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.netconf.client.NetconfClientSessionListener; + + +public class CallHomeMountSessionContextTest { + private Inet4Address someAddressIpv4; + private InetSocketAddress someSocketAddress; + private CallHomeChannelActivator mockActivator; + private CallHomeMountSessionContext.CloseCallback mockCallback; + private CallHomeMountSessionContext instance; + private CallHomeProtocolSessionContext mockProtocol; + + @Before + public void setup() throws UnknownHostException { + someAddressIpv4 = (Inet4Address) InetAddress.getByName("1.2.3.4"); + someSocketAddress = new InetSocketAddress(someAddressIpv4, 123); + + mockProtocol = mock(CallHomeProtocolSessionContext.class); + mockActivator = mock(CallHomeChannelActivator.class); + mockCallback = mock(CallHomeMountSessionContext.CloseCallback.class); + doReturn(someSocketAddress).when(mockProtocol).getRemoteAddress(); + + instance = new CallHomeMountSessionContext("test",mockProtocol, mockActivator, mockCallback); + } + + @Test + public void configNodeCanBeCreated() { + assertNotNull(instance.getConfigNode()); + } + + @Test + public void activationOfListenerSupportsSessionUp() { + // given + when(mockActivator.activate(any(NetconfClientSessionListener.class))) + .thenAnswer(invocationOnMock -> { + NetconfClientSession mockSession = mock(NetconfClientSession.class); + + Object arg = invocationOnMock.getArguments()[0]; + ((NetconfClientSessionListener) arg).onSessionUp(mockSession); + return null; + }); + + NetconfClientSessionListener mockListener = mock(NetconfClientSessionListener.class); + // when + mockActivator.activate(mockListener); + // then + verify(mockListener, times(1)).onSessionUp(any(NetconfClientSession.class)); + } + + @Test + public void activationOfListenerSupportsSessionTermination() { + // given + when(mockActivator.activate(any(NetconfClientSessionListener.class))) + .thenAnswer(invocationOnMock -> { + NetconfClientSession mockSession = mock(NetconfClientSession.class); + NetconfTerminationReason mockReason = mock(NetconfTerminationReason.class); + + Object arg = invocationOnMock.getArguments()[0]; + ((NetconfClientSessionListener) arg).onSessionTerminated(mockSession, mockReason); + return null; + }); + + NetconfClientSessionListener mockListener = mock(NetconfClientSessionListener.class); + // when + mockActivator.activate(mockListener); + // then + verify(mockListener, times(1)).onSessionTerminated(any(NetconfClientSession.class), + any(NetconfTerminationReason.class)); + } + + @Test + public void activationOfListenerSupportsSessionDown() { + // given + when(mockActivator.activate(any(NetconfClientSessionListener.class))) + .thenAnswer(invocationOnMock -> { + NetconfClientSession mockSession = mock(NetconfClientSession.class); + Exception mockException = mock(Exception.class); + + Object arg = invocationOnMock.getArguments()[0]; + ((NetconfClientSessionListener) arg).onSessionDown(mockSession, mockException); + return null; + }); + // given + NetconfClientSessionListener mockListener = mock(NetconfClientSessionListener.class); + // when + mockActivator.activate(mockListener); + // then + verify(mockListener, times(1)).onSessionDown(any(NetconfClientSession.class), any(Exception.class)); + } + + @Test + public void activationOfListenerSupportsSessionMessages() { + // given + when(mockActivator.activate(any(NetconfClientSessionListener.class))) + .thenAnswer(invocationOnMock -> { + NetconfClientSession mockSession = mock(NetconfClientSession.class); + NetconfMessage mockMsg = mock(NetconfMessage.class); + + Object arg = invocationOnMock.getArguments()[0]; + ((NetconfClientSessionListener) arg).onMessage(mockSession, mockMsg); + return null; + }); + // given + NetconfClientSessionListener mockListener = mock(NetconfClientSessionListener.class); + // when + mockActivator.activate(mockListener); + // then + verify(mockListener, times(1)).onMessage(any(NetconfClientSession.class), any(NetconfMessage.class)); + } +} diff --git a/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/ContextKeyTest.java b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/ContextKeyTest.java new file mode 100644 index 0000000000..e44ed2230e --- /dev/null +++ b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/ContextKeyTest.java @@ -0,0 +1,111 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import org.junit.Before; +import org.junit.Test; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddressBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber; +import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode; + + +public class ContextKeyTest { + IpAddress address1; + IpAddress address2; + + PortNumber port1; + PortNumber port2; + + NetconfNode mockNode; + NetconfClientSession mockSession; + + ContextKey instance1; + ContextKey instance2; + ContextKey instance3; + ContextKey instance4; + + @Before + public void setup() { + address1 = IpAddressBuilder.getDefaultInstance("1.2.3.4"); + address2 = IpAddressBuilder.getDefaultInstance("5.6.7.8"); + + port1 = new PortNumber(123); + port2 = new PortNumber(456); + + mockNode = mock(NetconfNode.class); + mockSession = mock(NetconfClientSession.class); + + instance1 = new ContextKey(address1, port1); + instance2 = new ContextKey(address2, port2); + instance3 = new ContextKey(address1, port2); + instance4 = new ContextKey(address2, port1); + + Host mockHost = mock(Host.class); + when(mockHost.getIpAddress()).thenReturn(address1); + when(mockNode.getHost()).thenReturn(mockHost); + + when(mockNode.getPort()).thenReturn(port1); + } + + @Test + public void hashCodesForDifferentKeysAreDifferent() { + // expect + assertNotEquals(instance1.hashCode(), instance2.hashCode()); + assertNotEquals(instance1.hashCode(), 0); + assertNotEquals(instance2.hashCode(), 0); + } + + @Test + public void variousFlavorsOfEqualWork() { + // expect + assertTrue(instance1.equals(instance1)); + assertFalse(instance1.equals(null)); + assertFalse(instance1.equals(new Long(123456))); + assertFalse(instance1.equals(instance2)); + assertFalse(instance1.equals(instance3)); + assertFalse(instance1.equals(instance4)); + } + + @Test + public void newContextCanBeCreatedFromASocketAddress() throws UnknownHostException { + // given + Inet4Address someAddressIpv4 = (Inet4Address) InetAddress.getByName("1.2.3.4"); + Inet6Address someAddressIpv6 = (Inet6Address) InetAddress.getByName("::1"); + // and + ContextKey key1 = ContextKey.from(new InetSocketAddress(someAddressIpv4, 123)); + ContextKey key2 = ContextKey.from(new InetSocketAddress(someAddressIpv6, 123)); + // expect + assertNotNull(key1); + assertNotNull(key1.toString()); + assertNotNull(key2); + assertNotNull(key2.toString()); + } + + @Test + public void newContextCanBeCreatedFromANetconfNode() { + // expect + assertNotNull(ContextKey.from(mockNode)); + } +} diff --git a/netconf/netconf-artifacts/pom.xml b/netconf/netconf-artifacts/pom.xml index 17db1dafb2..196f843cb4 100644 --- a/netconf/netconf-artifacts/pom.xml +++ b/netconf/netconf-artifacts/pom.xml @@ -195,6 +195,22 @@ ${project.version} + + org.opendaylight.netconf + callhome-protocol + ${project.version} + + + org.opendaylight.netconf + callhome-model + ${project.version} + + + org.opendaylight.netconf + callhome-provider + ${project.version} + + ${project.groupId} netconf-notifications-api diff --git a/netconf/pom.xml b/netconf/pom.xml index f6783c6219..1f80ee8e56 100644 --- a/netconf/pom.xml +++ b/netconf/pom.xml @@ -57,6 +57,10 @@ tools netconf-console + callhome-model + callhome-protocol + callhome-provider + netconf-artifacts diff --git a/restconf/models/ietf-yang-library/src/main/java/org/opendaylight/yang/gen/v1/urn/ietf/params/xml/ns/yang/ietf/yang/library/rev160621/module/list/CommonLeafsRevisionBuilder.java b/restconf/models/ietf-yang-library/src/main/java/org/opendaylight/yang/gen/v1/urn/ietf/params/xml/ns/yang/ietf/yang/library/rev160621/module/list/CommonLeafsRevisionBuilder.java index e0b6bfab93..5bd5bc1b9e 100644 --- a/restconf/models/ietf-yang-library/src/main/java/org/opendaylight/yang/gen/v1/urn/ietf/params/xml/ns/yang/ietf/yang/library/rev160621/module/list/CommonLeafsRevisionBuilder.java +++ b/restconf/models/ietf-yang-library/src/main/java/org/opendaylight/yang/gen/v1/urn/ietf/params/xml/ns/yang/ietf/yang/library/rev160621/module/list/CommonLeafsRevisionBuilder.java @@ -1,4 +1,5 @@ package org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev160621.module.list; + import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev160621.module.list.CommonLeafs.Revision;