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 <bvaradar@brocade.com>
Signed-off-by: Mike Arsenault <mike@mike-arsenault.com>
Signed-off-by: Allan Clarke <clarkea@brocade.com>
Signed-off-by: Ryan Goulding <ryandgoulding@gmail.com>
<groupId>${project.groupId}</groupId>
<artifactId>netconf-config</artifactId>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-model</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-protocol</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-provider</artifactId>
+ </dependency>
</dependencies>
<scm>
<feature name='odl-netconf-connector-all' version='${project.version}' description='OpenDaylight :: Netconf Connector :: All'>
<feature version='${project.version}'>odl-netconf-connector</feature>
<feature version='${project.version}'>odl-netconf-connector-ssh</feature>
+ <feature version='${project.version}'>odl-netconf-callhome-ssh</feature>
</feature>
<feature name='odl-message-bus' version='${project.version}'>
<bundle>mvn:org.opendaylight.netconf/netconf-connector-config/{{VERSION}}</bundle>
</feature>
+ <feature name='odl-netconf-callhome-ssh' version='${project.version}' description="OpenDaylight :: Netconf Connector :: Netconf Call Home Connector + Netconf SSH Server + loopback connection configuration">
+ <feature version='${project.version}'>odl-netconf-topology</feature>
+ <bundle>mvn:org.opendaylight.netconf/callhome-protocol/{{VERSION}}</bundle>
+ <bundle>mvn:org.opendaylight.netconf/callhome-provider/{{VERSION}}</bundle>
+ <bundle>mvn:org.opendaylight.netconf/callhome-model/{{VERSION}}</bundle>
+ </feature>
+
<feature name='odl-netconf-topology' version='${project.version}' description="OpenDaylight :: Netconf Topology :: Netconf Connector + Netconf SSH Server + Netconf configuration via config topology datastore">
<feature version='${project.version}'>odl-netconf-ssh</feature>
<feature version='${project.version}'>odl-netconf-connector</feature>
<type>xml</type>
<classifier>features</classifier>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>odl-netconf-callhome-ssh</artifactId>
+ <version>${project.version}</version>
+ <type>xml</type>
+ <classifier>features</classifier>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>odl-netconf-console</artifactId>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright © 2017 Red Hat, Inc. and others.
+
+ 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
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.opendaylight.odlparent</groupId>
+ <artifactId>single-feature-parent</artifactId>
+ <version>1.8.0-SNAPSHOT</version>
+ <relativePath/>
+ </parent>
+
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>odl-netconf-callhome-ssh</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ <packaging>feature</packaging>
+
+ <name>OpenDaylight :: Netconf Connector :: Netconf Callhome Connector + Netconf SSH Server + loopback connection configuration</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>odl-netconf-topology</artifactId>
+ <version>${project.version}</version>
+ <type>xml</type>
+ <classifier>features</classifier>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-connector-config</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-model</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-protocol</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-provider</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
<type>xml</type>
<classifier>features</classifier>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>odl-netconf-callhome-ssh</artifactId>
+ <version>${project.version}</version>
+ <type>xml</type>
+ <classifier>features</classifier>
+ </dependency>
</dependencies>
</project>
\ No newline at end of file
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.opendaylight.odlparent</groupId>
- <artifactId>odlparent-lite</artifactId>
+ <artifactId>odlparent</artifactId>
<version>1.8.0-SNAPSHOT</version>
<relativePath/>
</parent>
<version>1.2.0-SNAPSHOT</version>
<packaging>pom</packaging>
+ <properties>
+ <commons.opendaylight.version>1.8.0-SNAPSHOT</commons.opendaylight.version>
+ <controller.mdsal.version>1.5.0-SNAPSHOT</controller.mdsal.version>
+ <config.version>0.6.0-SNAPSHOT</config.version>
+ <features.test.version>1.8.0-SNAPSHOT</features.test.version>
+ <mdsal.version>2.2.0-SNAPSHOT</mdsal.version>
+ <mdsal.model.version>0.10.0-SNAPSHOT</mdsal.model.version>
+ <netconf.version>1.2.0-SNAPSHOT</netconf.version>
+ <netconf.connector.version>1.5.0-SNAPSHOT</netconf.connector.version>
+ <yangtools.version>1.1.0-SNAPSHOT</yangtools.version>
+ </properties>
+
<modules>
<module>features-netconf-connector</module>
<module>features4-netconf-connector</module>
<module>odl-netconf-connector</module>
<module>odl-netconf-connector-all</module>
<module>odl-netconf-connector-ssh</module>
+ <module>odl-netconf-callhome-ssh</module>
<module>odl-netconf-console</module>
<module>odl-netconf-topology</module>
</modules>
- <scm>
- <connection>scm:git:http://git.opendaylight.org/gerrit/controller.git</connection>
- <developerConnection>scm:git:ssh://git.opendaylight.org:29418/controller.git</developerConnection>
- <tag>HEAD</tag>
- <url>https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=summary</url>
- </scm>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-artifacts</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>features-yangtools</artifactId>
+ <version>${yangtools.version}</version>
+ <classifier>features</classifier>
+ <type>xml</type>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.controller</groupId>
+ <artifactId>features-mdsal</artifactId>
+ <version>${controller.mdsal.version}</version>
+ <classifier>features</classifier>
+ <type>xml</type>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.mdsal.model</groupId>
+ <artifactId>features-mdsal-model</artifactId>
+ <version>${mdsal.model.version}</version>
+ <classifier>features</classifier>
+ <type>xml</type>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>features-netconf</artifactId>
+ <classifier>features</classifier>
+ <type>xml</type>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>sal-netconf-connector</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>messagebus-netconf</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-console</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-topology</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-topology-config</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-tcp</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-ssh</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ <version>${bouncycastle.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ <version>${bouncycastle.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-topology-singleton</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-model</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-provider</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-protocol</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-connector-config</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>netconf-config</artifactId>
+ </dependency>
+ </dependencies>
+
+ <scm>
+ <connection>scm:git:http://git.opendaylight.org/gerrit/controller.git</connection>
+ <developerConnection>scm:git:ssh://git.opendaylight.org:29418/controller.git</developerConnection>
+ <tag>HEAD</tag>
+ <url>https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=summary</url>
+ </scm>
</project>
<groupId>org.opendaylight.netconf</groupId>
<artifactId>mdsal-netconf-impl</artifactId>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-model</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-protocol</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>callhome-provider</artifactId>
+ </dependency>
</dependencies>
<scm>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (c) 2015 Cisco Systems, Inc. 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
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>binding-parent</artifactId>
+ <version>0.10.0-SNAPSHOT</version>
+ <relativePath/>
+ </parent>
+
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>callhome-model</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ <packaging>bundle</packaging>
+ <name>${project.artifactId}</name>
+</project>
--- /dev/null
+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;
+ }
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (c) 2016 Cisco Systems, Inc. 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
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>binding-parent</artifactId>
+ <version>0.10.0-SNAPSHOT</version>
+ <relativePath/>
+ </parent>
+
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>callhome-protocol</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ <name>${project.artifactId}</name>
+ <packaging>bundle</packaging>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-subsystem</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-client</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk15on</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>mockito-configuration</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+
+</project>
--- /dev/null
+/*
+ * 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<String,String> 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);
+ }
+}
--- /dev/null
+/*
+ * 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<CallHomeAuthorization> {
+
+ private final String nodeId;
+ private final String username;
+ private Set<String> passwords = new HashSet<>();
+ private Set<KeyPair> 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<String> passwords;
+ private final Set<KeyPair> clientKeyPair;
+
+ ServerAllowed(String nodeId, String username, Collection<String >passwords, Collection<KeyPair> 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);
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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<NetconfClientSession> activate(@Nonnull NetconfClientSessionListener listener);
+
+}
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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();
+
+}
--- /dev/null
+/*
+ * 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<CallHomeSessionContext> 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<OpenFuture> 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<NetconfClientSession> 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<NetconfClientSession> 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<NetconfClientSession> 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<String, CallHomeSessionContext> 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;
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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.");
+ }
+
+ }
+}
--- /dev/null
+/*
+ * 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<AuthFuture> newAuthSshFutureListener(final ClientSession cSession) {
+ return new SshFutureListener<AuthFuture>() {
+ @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);
+ }
+}
--- /dev/null
+/*
+ * 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<NetconfCallHomeServer> {
+
+ 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.<NetconfHelloMessageAdditionalHeader>absent(), DEFAULT_SESSION_TIMEOUT_MILLIS);
+ }
+
+ private EventLoopGroup defaultNettyGroup() {
+ return new LocalEventLoopGroup();
+ }
+
+ private InetSocketAddress defaultBindAddress() {
+ return new InetSocketAddress(DEFAULT_CALL_HOME_PORT);
+ }
+
+}
--- /dev/null
+/*
+ * 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<NetconfClientSession>
+ implements SessionListenerFactory<NetconfClientSessionListener> {
+
+ 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<NetconfClientSession> promise) {
+ ch.pipeline().addAfter(NETCONF_MESSAGE_DECODER, AbstractChannelInitializer.NETCONF_SESSION_NEGOTIATOR,
+ negotiatorFactory.getSessionNegotiator(this, ch, promise));
+ }
+
+}
--- /dev/null
+/*
+ * 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("");
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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<OpenFuture> listener = instance.newSshFutureListener(mockChannel);
+ // when
+ listener.operationComplete(mockFuture);
+ // then
+ verify(mockPipeline, times(1)).fireChannelActive();
+ }
+
+ @Test
+ @Ignore
+ public void failureToOpenTheChannelShouldCauseTheSessionToClose() {
+ // given
+ SshFutureListener<OpenFuture> 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());
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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<String,String> 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);
+ }
+
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.opendaylight.mdsal</groupId>
+ <artifactId>binding-parent</artifactId>
+ <version>0.10.0-SNAPSHOT</version>
+ <relativePath/>
+ </parent>
+
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>callhome-provider</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ <packaging>bundle</packaging>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-subsystem</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.opendaylight.controller</groupId>
+ <artifactId>sal-binding-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.controller</groupId>
+ <artifactId>sal-binding-broker-impl</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-topology</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>callhome-protocol</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>callhome-model</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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> GLOBAL_PATH =
+ InstanceIdentifier.create(NetconfCallhomeServer.class).child(Global.class);
+ private static final DataTreeIdentifier<Global> GLOBAL =
+ new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION, GLOBAL_PATH);
+
+ private static final InstanceIdentifier<Device> ALLOWED_DEVICES_PATH =
+ InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class);
+ private static final DataTreeIdentifier<Device> ALLOWED_DEVICES =
+ new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION, ALLOWED_DEVICES_PATH);
+
+ private final GlobalConfig globalConfig = new GlobalConfig();
+ private final DeviceConfig deviceConfig = new DeviceConfig();
+ private final ListenerRegistration<GlobalConfig> configReg;
+ private final ListenerRegistration<DeviceConfig> 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<Device> {
+
+ private ConcurrentMap<PublicKey, Device> byPublicKey = new ConcurrentHashMap<PublicKey, Device>();
+
+ @Override
+ public void onDataTreeChanged(Collection<DataTreeModification<Device>> arg0) {
+ for (DataTreeModification<Device> dataTreeModification : arg0) {
+ DataObjectModification<Device> rootNode = dataTreeModification.getRootNode();
+ process(rootNode);
+ }
+ }
+
+ private void process(DataObjectModification<Device> 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<Global> {
+
+
+ private volatile Global current = null;
+
+ @Override
+ public void onDataTreeChanged(Collection<DataTreeModification<Global>> arg0) {
+ for (DataTreeModification<Global> 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;
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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<NetconfClientSession> createClient(NetconfClientConfiguration clientConfiguration) {
+ return activateChannel(clientConfiguration);
+ }
+
+ @Override
+ public Future<Void> createReconnectingClient(NetconfReconnectingClientConfiguration clientConfiguration) {
+ return activateChannel(clientConfiguration);
+ }
+
+ private <V> Future<V> 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;
+ }
+}
--- /dev/null
+/*
+ * 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")
+ <V> Promise<V> activateNetconfChannel(NetconfClientSessionListener sessionListener) {
+ return (Promise<V>) 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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<SocketAddress, CallHomeMountSessionContext> contextByAddress = new ConcurrentHashMap<>();
+ private final Multimap<PublicKey, CallHomeMountSessionContext> contextByPublicKey = MultimapBuilder.hashKeys().hashSetValues().build();
+
+ @Nullable
+ public CallHomeMountSessionContext getByAddress(InetSocketAddress remoteAddr) {
+ return contextByAddress.get(remoteAddr);
+ }
+
+ @Nullable
+ public Collection<CallHomeMountSessionContext> 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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<NetconfSessionPreferences> createSalFacade(RemoteDeviceId id, Broker domBroker,
+ BindingAwareBroker bindingBroker) {
+ return new NetconfDeviceSalFacade(id, domBroker, bindingAwareBroker);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+ xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
+ odl:use-default-for-reference-types="true">
+
+ <reference id="bindingAwareBroker"
+ interface="org.opendaylight.controller.sal.binding.api.BindingAwareBroker"/>
+ <reference id="keepAliveExecutor"
+ interface="org.opendaylight.controller.config.threadpool.ScheduledThreadPool"
+ odl:type="global-netconf-ssh-scheduled-executor"/>
+ <reference id="processingExecutor"
+ interface="org.opendaylight.controller.config.threadpool.ThreadPool"
+ odl:type="global-netconf-processing-executor"/>
+ <reference id="domBroker"
+ interface="org.opendaylight.controller.sal.core.api.Broker"/>
+ <reference id="eventExecutor"
+ interface="io.netty.util.concurrent.EventExecutor"
+ odl:type="global-event-executor"/>
+ <reference id="dataBroker"
+ interface="org.opendaylight.controller.md.sal.binding.api.DataBroker"/>
+ <reference id="domMountPointService"
+ interface="org.opendaylight.controller.md.sal.dom.api.DOMMountPointService"/>
+
+ <bean id="schemaRepository" class="org.opendaylight.netconf.callhome.mount.SchemaRepositoryProviderImpl">
+ <argument value="shared-schema-repository-impl"/>
+ </bean>
+
+ <bean id="callhomeProvider" class="org.opendaylight.netconf.callhome.mount.IetfZeroTouchCallHomeServerProvider"
+ init-method="init"
+ destroy-method="close" >
+ <argument ref="dataBroker" />
+ <argument ref="callhomeDispatcher" />
+ </bean>
+
+ <bean id="callhomeDispatcher" class="org.opendaylight.netconf.callhome.mount.CallHomeMountDispatcher">
+ <argument value="topology-netconf"/>
+ <argument ref="bindingAwareBroker"/>
+ <argument ref="eventExecutor"/>
+ <argument ref="keepAliveExecutor"/>
+ <argument ref="processingExecutor"/>
+ <argument ref="schemaRepository"/>
+ <argument ref="domBroker"/>
+ <argument ref="dataBroker"/>
+ <argument ref="domMountPointService"/>
+ </bean>
+
+
+</blueprint>
\ No newline at end of file
--- /dev/null
+/*
+ * 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<NetconfClientSession> 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));
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>callhome-protocol</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>callhome-model</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>callhome-provider</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netconf-notifications-api</artifactId>
<module>tools</module>
<module>netconf-console</module>
+ <module>callhome-model</module>
+ <module>callhome-protocol</module>
+ <module>callhome-provider</module>
+
<module>netconf-artifacts</module>
</modules>
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;