BUG-1422 Introduce Call-Home functionality for the NETCONF Topology. 53/49253/21
authorBalaji <bvaradar@brocade.com>
Wed, 11 Jan 2017 17:04:27 +0000 (09:04 -0800)
committerColin Dixon <colin@colindixon.com>
Fri, 24 Mar 2017 15:50:01 +0000 (11:50 -0400)
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>
44 files changed:
features/netconf-connector/features-netconf-connector/pom.xml
features/netconf-connector/features-netconf-connector/src/main/features/features.xml
features/netconf-connector/features4-netconf-connector/pom.xml
features/netconf-connector/odl-netconf-callhome-ssh/pom.xml [new file with mode: 0644]
features/netconf-connector/odl-netconf-connector-all/pom.xml
features/netconf-connector/pom.xml
features/netconf/features-netconf/pom.xml
netconf/callhome-model/pom.xml [new file with mode: 0644]
netconf/callhome-model/src/main/yang/odl-netconf-callhome-server.yang [new file with mode: 0644]
netconf/callhome-protocol/pom.xml [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoder.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorization.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationProvider.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeChannelActivator.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeNetconfSubsystemListener.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeProtocolSessionContext.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContext.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannel.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServer.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerBuilder.java [new file with mode: 0644]
netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/ReverseSshChannelInitializer.java [new file with mode: 0644]
netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoderTest.java [new file with mode: 0644]
netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationTest.java [new file with mode: 0644]
netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContextTest.java [new file with mode: 0644]
netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannelTest.java [new file with mode: 0644]
netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerTest.java [new file with mode: 0644]
netconf/callhome-provider/pom.xml [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/BaseCallHomeTopology.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeAuthProviderImpl.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcher.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContext.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionManager.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeTopology.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/Configuration.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/ContextKey.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/IetfZeroTouchCallHomeServerProvider.java [new file with mode: 0644]
netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/SchemaRepositoryProviderImpl.java [new file with mode: 0644]
netconf/callhome-provider/src/main/resources/org/opendaylight/blueprint/callhome-topology.xml [new file with mode: 0755]
netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcherTest.java [new file with mode: 0644]
netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContextTest.java [new file with mode: 0644]
netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/ContextKeyTest.java [new file with mode: 0644]
netconf/netconf-artifacts/pom.xml
netconf/pom.xml
restconf/models/ietf-yang-library/src/main/java/org/opendaylight/yang/gen/v1/urn/ietf/params/xml/ns/yang/ietf/yang/library/rev160621/module/list/CommonLeafsRevisionBuilder.java

index 0750c52c2d0ec28f0c2ee5db90cc6aae50d9c320..9e90e290963f2d5f2befec2052262dfb3e57bca5 100644 (file)
       <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>
index c6a4263fbfc9f3f647a39f7b04145b02bf7706de..a3d60f24cad204862c6b392b391048a0147e3933 100644 (file)
@@ -19,6 +19,7 @@
     <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>
index 6b5b5372f737122671adb0234cc84156f2f30e32..32ef0d3783a48a80dbb2f05447068ab7c85a7e9d 100644 (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>
         <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>odl-netconf-console</artifactId>
diff --git a/features/netconf-connector/odl-netconf-callhome-ssh/pom.xml b/features/netconf-connector/odl-netconf-callhome-ssh/pom.xml
new file mode 100644 (file)
index 0000000..d66b6ca
--- /dev/null
@@ -0,0 +1,57 @@
+<?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
index 7f0ebe0aa51ff3bb60b82ab5ac8eb3faff34ea89..83c52e39b41d4aef775d810eadb241f8403ee7cc 100644 (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
index 2928bb3aa5205f4be4d86f2057bf8f2e5169517d..2081582ce359bc8da0e7a897a26b126c5157f644 100644 (file)
@@ -11,7 +11,7 @@
     <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>
index 0eb098842bb705930d4dbc274a2ae8ab1573fb19..ab170df881914b5ecacdbec59349abb319f48a81 100644 (file)
       <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>
diff --git a/netconf/callhome-model/pom.xml b/netconf/callhome-model/pom.xml
new file mode 100644 (file)
index 0000000..8756b94
--- /dev/null
@@ -0,0 +1,24 @@
+<?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>
diff --git a/netconf/callhome-model/src/main/yang/odl-netconf-callhome-server.yang b/netconf/callhome-model/src/main/yang/odl-netconf-callhome-server.yang
new file mode 100644 (file)
index 0000000..52a9b0f
--- /dev/null
@@ -0,0 +1,65 @@
+module odl-netconf-callhome-server {
+
+  namespace "urn:opendaylight:params:xml:ns:yang:netconf-callhome-server";
+  prefix "callhome";
+
+  organization
+   "OpenDaylight Project";
+
+  contact
+   "netconf-dev@lists.opendaylight.org";
+
+  description
+   "This module defines the northbound interface for OpenDaylight NETCONF Callhome.";
+
+  revision "2016-11-09" {
+    description "Initial version";
+  }
+
+  grouping credentials {
+    container credentials {
+      presence "Credentials to device.";
+      leaf username {
+        mandatory true;
+        description "Username to be used for authentication";
+        type string {
+          length "1..max";
+        }
+      }
+      leaf-list passwords {
+        description "Passwords to be used for authentication.";
+        type string;
+      }
+    }
+  }
+
+  container netconf-callhome-server {
+    description "Settings for call home server administration";
+
+    container global {
+      presence "global credentials are enabled.";
+      uses credentials;
+      leaf accept-all-ssh-keys {
+        type boolean;
+        default false;
+      }
+    }
+
+    container allowed-devices {
+      description "A list of allowed devices";
+      list device {
+        key unique-id;
+        leaf unique-id {
+          description "Identifier of device, which will be used to identify device.";
+          type string;
+        }
+        leaf ssh-host-key {
+          description "BASE-64 encoded public key which device will use during connection.";
+          type string;
+        }
+        unique ssh-host-key;
+        uses credentials;
+      }
+    }
+  }
+}
diff --git a/netconf/callhome-protocol/pom.xml b/netconf/callhome-protocol/pom.xml
new file mode 100644 (file)
index 0000000..3e68f86
--- /dev/null
@@ -0,0 +1,66 @@
+<?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>
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoder.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoder.java
new file mode 100644 (file)
index 0000000..ff6d380
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.callhome.protocol;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PublicKey;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.sshd.common.util.Base64;
+import org.apache.sshd.common.util.SecurityUtils;
+import org.bouncycastle.jce.ECNamedCurveTable;
+import org.bouncycastle.jce.ECPointUtil;
+import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
+import org.bouncycastle.jce.spec.ECNamedCurveSpec;
+
+/**
+ *
+ * FIXME: This should be probably located at AAA library
+ *
+ */
+public class AuthorizedKeysDecoder {
+
+    private static final String KEY_FACTORY_TYPE_RSA = "RSA";
+    private static final String KEY_FACTORY_TYPE_DSA = "DSA";
+    private static final String KEY_FACTORY_TYPE_ECDSA = "EC";
+
+    private static Map<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);
+    }
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorization.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorization.java
new file mode 100644 (file)
index 0000000..9cce173
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.callhome.protocol;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import java.security.KeyPair;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.client.session.ClientSessionImpl;
+
+/**
+ *
+ * Authorization context for incoming call home sessions.
+ *
+ * @see CallHomeAuthorizationProvider
+ */
+public abstract class CallHomeAuthorization {
+    private static final CallHomeAuthorization REJECTED = new CallHomeAuthorization() {
+
+        @Override
+        public boolean isServerAllowed() {
+            return false;
+        }
+
+        @Override
+        protected String getSessionName(){
+            return "";
+        }
+
+        @Override
+        protected void applyTo(ClientSession session) {
+            throw new IllegalStateException("Server is not allowed.");
+        }
+    };
+
+    /**
+     *
+     * Returns CallHomeAuthorization object with intent to
+     * reject incoming connection.
+     *
+     * {@link CallHomeAuthorizationProvider} may use returned object
+     * as return value for {@link CallHomeAuthorizationProvider#provideAuth(java.net.SocketAddress, java.security.PublicKey)}
+     * if the incoming session should be rejected due to policy implemented
+     * by provider.
+     *
+     * @return CallHomeAuthorization with {@code isServerAllowed() == false}
+     */
+    public static final CallHomeAuthorization rejected() {
+        return REJECTED;
+    }
+
+    /**
+     * Creates a builder for CallHomeAuthorization with intent
+     * to accept incoming connection and to provide credentials.
+     *
+     * Note: If session with same sessionName is already opened and
+     * active, incoming session will be rejected.
+     *
+     * @param sessionName Application specific unique identifier for incoming session
+     * @param username Username to be used for authorization
+     * @return Builder which allows to specify credentials.
+     */
+    public static final Builder serverAccepted(String sessionName, String username) {
+        return new Builder(sessionName, username);
+    }
+
+    /**
+     * Returns true if incomming connection is allowed.
+     *
+     * @return true if incoming connection from SSH Server is allowed.
+     */
+    public abstract boolean isServerAllowed();
+
+    /**
+     *
+     * Applies provided authentification to Mina SSH Client Session
+     *
+     * @param session Client Session to which authorization parameters will by applied
+     */
+    protected abstract void applyTo(ClientSession session);
+
+    protected abstract String getSessionName();
+
+    /**
+     *
+     * Builder for CallHomeAuthorization which accepts incoming connection.
+     *
+     * Use {@link CallHomeAuthorization#serverAccepted(String, String)} to instantiate
+     * builder.
+     *
+     */
+    public static class Builder implements org.opendaylight.yangtools.concepts.Builder<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);
+            }
+        }
+    }
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationProvider.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationProvider.java
new file mode 100644 (file)
index 0000000..a07a925
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.callhome.protocol;
+
+import java.net.SocketAddress;
+import java.security.PublicKey;
+import javax.annotation.Nonnull;
+
+/**
+ *
+ * Provider responsible for resolving CallHomeAuthorization
+ *
+ */
+public interface CallHomeAuthorizationProvider {
+
+    /**
+     *
+     * Provides authorization parameters for incoming call-home connection.
+     *
+     * @param remoteAddress Remote socket address of incoming connection
+     * @param serverKey SSH key provided by SSH server on incoming connection
+     *
+     * @return {@link CallHomeAuthorization} with authorization information.
+     *
+     */
+    @Nonnull
+    CallHomeAuthorization provideAuth(@Nonnull SocketAddress remoteAddress,@Nonnull PublicKey serverKey);
+
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeChannelActivator.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeChannelActivator.java
new file mode 100644 (file)
index 0000000..f64ffc3
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.callhome.protocol;
+
+import io.netty.util.concurrent.Promise;
+import javax.annotation.Nonnull;
+import org.opendaylight.netconf.client.NetconfClientSession;
+import org.opendaylight.netconf.client.NetconfClientSessionListener;
+
+/**
+ * Activator of NETCONF channel on incoming SSH Call Home session.
+ *
+ */
+public interface CallHomeChannelActivator {
+
+    /**
+     *
+     * Activates Netconf Client Channel with supplied client session listener.
+     *
+     * Activation of channel will result in start of NETCONF client
+     * session negotiation on underlying ssh channel.
+     *
+     * @param listener Client Session Listener to be attached to NETCONF session.
+     * @return Promise with negotiated NETCONF session
+     */
+    @Nonnull
+    Promise<NetconfClientSession> activate(@Nonnull NetconfClientSessionListener listener);
+
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeNetconfSubsystemListener.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeNetconfSubsystemListener.java
new file mode 100644 (file)
index 0000000..74d9344
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.callhome.protocol;
+
+import org.opendaylight.netconf.client.NetconfClientSessionListener;
+
+/**
+ *
+ * Listener for successful opening of NETCONF channel on incoming Call Home connections.
+ *
+ */
+public interface CallHomeNetconfSubsystemListener {
+
+    /**
+     * Invoked when Netconf Subsystem was successfully opened on incoming SSH Call Home connection.
+     *
+     * Implementors of this method should use provided {@link CallHomeChannelActivator} to attach
+     * {@link NetconfClientSessionListener} to session and to start NETCONF client session negotiation.
+     *
+     *
+     * @param session Incoming Call Home session on which NETCONF subsystem was successfully opened
+     * @param activator Channel Activator to be used in order to start NETCONF Session negotiation.
+     */
+    void onNetconfSubsystemOpened(CallHomeProtocolSessionContext session, CallHomeChannelActivator activator);
+
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeProtocolSessionContext.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeProtocolSessionContext.java
new file mode 100644 (file)
index 0000000..a2e1cb1
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.callhome.protocol;
+
+import java.net.InetSocketAddress;
+import java.security.PublicKey;
+
+/**
+ *
+ * Protocol level Session Context for incoming Call Home connections.
+ *
+ */
+public interface CallHomeProtocolSessionContext {
+
+    /**
+     * Returns session identifier provided by  CallHomeAuthorizationProvider
+     *
+     * @return Returns application-provided session identifier
+     */
+    String getSessionName();
+
+    /**
+     * Returns public key provided by remote SSH Server for this session.
+     *
+     * @return public key provided by remote SSH Server
+     */
+    PublicKey getRemoteServerKey();
+
+    /**
+     *
+     * Returns remote socket address associated with this session.
+     *
+     * @return remote socket address associated with this session.
+     */
+    InetSocketAddress getRemoteAddress();
+
+    /**
+     * Returns version string provided by remote server.
+     *
+     * @return Version string provided by remote server.
+     */
+    String getRemoteServerVersion();
+
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContext.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContext.java
new file mode 100644 (file)
index 0000000..8d844c7
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import io.netty.channel.EventLoopGroup;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import io.netty.util.concurrent.Promise;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.PublicKey;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import org.apache.sshd.ClientChannel;
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.client.future.AuthFuture;
+import org.apache.sshd.client.future.OpenFuture;
+import org.apache.sshd.client.session.ClientSessionImpl;
+import org.apache.sshd.common.Session;
+import org.apache.sshd.common.future.SshFutureListener;
+import org.opendaylight.netconf.client.NetconfClientSession;
+import org.opendaylight.netconf.client.NetconfClientSessionListener;
+import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class CallHomeSessionContext implements CallHomeProtocolSessionContext {
+
+    private static final Logger LOG = LoggerFactory.getLogger(CallHomeSessionContext.class);
+    static final Session.AttributeKey<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;
+        }
+
+    }
+
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannel.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannel.java
new file mode 100644 (file)
index 0000000..e5f5f95
--- /dev/null
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import com.google.common.base.Preconditions;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.AbstractServerChannel;
+import io.netty.channel.ChannelConfig;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelMetadata;
+import io.netty.channel.ChannelOutboundBuffer;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultChannelConfig;
+import io.netty.channel.EventLoop;
+import java.net.SocketAddress;
+import org.apache.sshd.ClientChannel;
+import org.apache.sshd.ClientSession;
+import org.opendaylight.netconf.nettyutil.handler.ssh.client.AsyncSshHandlerReader;
+import org.opendaylight.netconf.nettyutil.handler.ssh.client.AsyncSshHandlerReader.ReadMsgHandler;
+import org.opendaylight.netconf.nettyutil.handler.ssh.client.AsyncSshHandlerWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class MinaSshNettyChannel extends AbstractServerChannel {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MinaSshNettyChannel.class);
+    private static final ChannelMetadata METADATA = new ChannelMetadata(false);
+    private final ChannelConfig config = new DefaultChannelConfig(this);
+    private final CallHomeSessionContext context;
+    private final ClientSession session;
+    private final ClientChannel sshChannel;
+    private final AsyncSshHandlerReader  sshReadHandler;
+    private final AsyncSshHandlerWriter sshWriteAsyncHandler;
+
+
+    private volatile boolean nettyClosed = false;
+
+    MinaSshNettyChannel(CallHomeSessionContext context, ClientSession session, ClientChannel sshChannel) {
+        this.context = Preconditions.checkNotNull(context);
+        this.session = Preconditions.checkNotNull(session);
+        this.sshChannel = Preconditions.checkNotNull(sshChannel);
+        this.sshReadHandler = new AsyncSshHandlerReader(new ConnectionClosedDuringRead(), new FireReadMessage(), "netconf",
+                sshChannel.getAsyncOut());
+        this.sshWriteAsyncHandler = new AsyncSshHandlerWriter(sshChannel.getAsyncIn());
+        pipeline().addFirst(createChannelAdapter());
+    }
+
+    private ChannelOutboundHandlerAdapter createChannelAdapter() {
+        return new ChannelOutboundHandlerAdapter() {
+
+            @Override
+            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+                sshWriteAsyncHandler.write(ctx, msg, promise);
+            }
+
+        };
+    }
+
+    @Override
+    public ChannelConfig config() {
+        return config;
+    }
+
+    private boolean notClosing(org.apache.sshd.common.Closeable sshCloseable) {
+        return !sshCloseable.isClosing() && !sshCloseable.isClosed();
+    }
+
+
+    @Override
+    public boolean isOpen() {
+        return notClosing(session);
+    }
+
+    @Override
+    public boolean isActive() {
+        return notClosing(session);
+    }
+
+    @Override
+    public ChannelMetadata metadata() {
+        return METADATA;
+    }
+
+    @Override
+    protected AbstractUnsafe newUnsafe() {
+       return new SshUnsafe();
+    }
+
+    @Override
+    protected boolean isCompatible(EventLoop loop) {
+        return true;
+    }
+
+    @Override
+    protected SocketAddress localAddress0() {
+        return session.getIoSession().getLocalAddress();
+    }
+
+    @Override
+    protected SocketAddress remoteAddress0() {
+        return context.getRemoteAddress();
+    }
+
+    @Override
+    protected void doBind(SocketAddress localAddress) throws Exception {
+        throw new UnsupportedOperationException("Bind not supported.");
+    }
+
+    void doMinaDisconnect(boolean blocking) {
+        if(notClosing(session)) {
+            sshChannel.close(blocking);
+            session.close(blocking);
+        }
+    }
+
+    void doNettyDisconnect() {
+        if(! nettyClosed) {
+            nettyClosed = true;
+            pipeline().fireChannelInactive();
+            sshReadHandler.close();
+            sshWriteAsyncHandler.close();
+        }
+    }
+
+    @Override
+    protected void doDisconnect() throws Exception {
+        LOG.info("Disconnect invoked");
+        doNettyDisconnect();
+        doMinaDisconnect(false);
+    }
+
+    @Override
+    protected void doClose() throws Exception {
+        context.removeSelf();
+        if(notClosing(session)) {
+            session.close(true);
+            sshChannel.close(true);
+        }
+    }
+
+    @Override
+    protected void doBeginRead() throws Exception {
+        // Intentional NOOP - read is started by AsyncSshHandlerReader
+    }
+
+    @Override
+    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
+        throw new IllegalStateException("Outbound writes to SSH should be done by SSH Write handler");
+    }
+
+    private final class FireReadMessage implements ReadMsgHandler {
+
+        @Override
+        public void onMessageRead(ByteBuf msg) {
+            pipeline().fireChannelRead(msg);
+        }
+
+    }
+
+    private final class ConnectionClosedDuringRead implements AutoCloseable {
+
+        /**
+         * Invoked when SSH session dropped during read using {@link AsyncSshHandlerReader}
+         */
+        @Override
+        public void close() throws Exception {
+            doNettyDisconnect();
+        }
+
+    }
+
+    private class SshUnsafe extends AbstractUnsafe {
+
+        @Override
+        public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
+            throw new UnsupportedOperationException("Unsafe is not supported.");
+        }
+
+    }
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServer.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServer.java
new file mode 100644 (file)
index 0000000..460c742
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.PublicKey;
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.SshClient;
+import org.apache.sshd.client.ServerKeyVerifier;
+import org.apache.sshd.client.SessionFactory;
+import org.apache.sshd.client.future.AuthFuture;
+import org.apache.sshd.common.Session;
+import org.apache.sshd.common.SessionListener;
+import org.apache.sshd.common.future.SshFutureListener;
+import org.apache.sshd.common.io.IoAcceptor;
+import org.apache.sshd.common.io.IoServiceFactory;
+import org.apache.sshd.common.io.mina.MinaServiceFactory;
+import org.apache.sshd.common.io.nio2.Nio2ServiceFactory;
+import org.opendaylight.netconf.callhome.protocol.CallHomeSessionContext.Factory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NetconfCallHomeServer implements AutoCloseable, ServerKeyVerifier {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NetconfCallHomeServer.class);
+
+    private final IoAcceptor acceptor;
+    private final SshClient client;
+    private final CallHomeAuthorizationProvider authProvider;
+    private final CallHomeSessionContext.Factory sessionFactory;
+    private final InetSocketAddress bindAddress;
+
+    NetconfCallHomeServer(SshClient sshClient, CallHomeAuthorizationProvider authProvider, Factory factory,
+            InetSocketAddress socketAddress) {
+        this.client = Preconditions.checkNotNull(sshClient);
+        this.authProvider = Preconditions.checkNotNull(authProvider);
+        this.sessionFactory = Preconditions.checkNotNull(factory);
+        this.bindAddress = socketAddress;
+
+        sshClient.setServerKeyVerifier(this);
+
+        SessionFactory clientSessions = new SessionFactory();
+        clientSessions.setClient(sshClient);
+        clientSessions.addListener(createSessionListener());
+
+        IoServiceFactory minaFactory = createServiceFactory(sshClient);
+        this.acceptor = minaFactory.createAcceptor(clientSessions);
+    }
+
+    IoServiceFactory createServiceFactory(SshClient sshClient) {
+        try {
+            return createMinaServiceFactory(sshClient);
+        } catch (NoClassDefFoundError e) {
+            LOG.warn("Mina is not available, defaulting to NIO.");
+            return new Nio2ServiceFactory(sshClient);
+        }
+    }
+
+
+    protected IoServiceFactory createMinaServiceFactory(SshClient sshClient) {
+        return new MinaServiceFactory(sshClient);
+    }
+
+    SessionListener createSessionListener() {
+        return new SessionListener() {
+
+            @Override
+            public void sessionEvent(Session session, Event event) {
+                ClientSession cSession = (ClientSession) session;
+                LOG.debug("SSH session {} event {}", session, event);
+                switch (event) {
+                    case KeyEstablished:
+                        doAuth(cSession);
+                        break;
+                    case Authenticated:
+                        doPostAuth(cSession);
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            @Override
+            public void sessionCreated(Session session) {
+                LOG.debug("SSH session {} created", session);
+            }
+
+            @Override
+            public void sessionClosed(Session session) {
+                CallHomeSessionContext ctx = CallHomeSessionContext.getFrom((ClientSession) session);
+                if(ctx != null) {
+                    ctx.removeSelf();
+                }
+                LOG.debug("SSH Session {} closed", session);
+            }
+        };
+    }
+
+    private void doPostAuth(final ClientSession cSession) {
+        CallHomeSessionContext.getFrom(cSession).openNetconfChannel();
+    }
+
+    private void doAuth(final ClientSession cSession) {
+        try {
+            final AuthFuture authFuture = CallHomeSessionContext.getFrom(cSession).authorize();
+            authFuture.addListener(newAuthSshFutureListener(cSession));
+        } catch (IOException e) {
+            LOG.error("Failed to authorize session {}", cSession, e);
+        }
+    }
+
+    SshFutureListener<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);
+    }
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerBuilder.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerBuilder.java
new file mode 100644 (file)
index 0000000..f925246
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import com.google.common.base.Optional;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.local.LocalEventLoopGroup;
+import io.netty.util.HashedWheelTimer;
+import java.net.InetSocketAddress;
+import java.util.concurrent.TimeUnit;
+import org.apache.sshd.SshClient;
+import org.opendaylight.netconf.api.messages.NetconfHelloMessageAdditionalHeader;
+import org.opendaylight.netconf.callhome.protocol.CallHomeSessionContext.Factory;
+import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
+import org.opendaylight.yangtools.concepts.Builder;
+
+public class NetconfCallHomeServerBuilder implements Builder<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);
+    }
+
+}
diff --git a/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/ReverseSshChannelInitializer.java b/netconf/callhome-protocol/src/main/java/org/opendaylight/netconf/callhome/protocol/ReverseSshChannelInitializer.java
new file mode 100644 (file)
index 0000000..009cf21
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import io.netty.channel.Channel;
+import io.netty.util.concurrent.Promise;
+import org.opendaylight.netconf.client.NetconfClientSession;
+import org.opendaylight.netconf.client.NetconfClientSessionListener;
+import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
+import org.opendaylight.netconf.nettyutil.AbstractChannelInitializer;
+import org.opendaylight.protocol.framework.SessionListenerFactory;
+
+class ReverseSshChannelInitializer extends AbstractChannelInitializer<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));
+    }
+
+}
diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoderTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/AuthorizedKeysDecoderTest.java
new file mode 100644 (file)
index 0000000..20cab64
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import static org.junit.Assert.assertEquals;
+
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.opendaylight.netconf.callhome.protocol.AuthorizedKeysDecoder;
+
+
+public class AuthorizedKeysDecoderTest {
+
+    AuthorizedKeysDecoder instance;
+
+    @Before
+    public void setup() {
+        instance = new AuthorizedKeysDecoder();
+    }
+
+    @Test
+    public void authorizedKeysDecoderValidRSAKey () throws GeneralSecurityException {
+        // given
+        String rsaStr = "AAAAB3NzaC1yc2EAAAADAQABAAABAQCvLigTfPZMqOQwHp051Co4lwwPwO21NFIXWgjQmCPEgRTqQpei7qQaxlLGkrIPjZtJQRgCuC+Sg8HFw1YpUaMybN0nFInInQLp/qe0yc9ByDZM2G86NX6W5W3+j87I8Fh1dnMov1iJ0DFVn8RLwdEGjreiZCRyJOMuHghh6y4EG7W8BwmZrse17zhSpc2wFOVhxeZnYAQFEw6g48LutFRDpoTjGgz1nz/L4zcaUxxigs8wdY+qTTOHxSTxlLqwSZPFLyYrV2KJ9mKahMuYUy6o2b8snsjvnSjyK0kY+U0C6c8fmPDFUc0RqJqfdnsIUyh11U8d3NZdaFWg0UW0SNK3";
+        // when
+        PublicKey serverKey = instance.decodePublicKey(rsaStr);
+        // then
+        assertEquals(serverKey.getAlgorithm(), "RSA");
+    }
+
+    @Test(expected = Exception.class)
+    public void authorizedKeysDecoderInvalidRSAKey () throws GeneralSecurityException {
+        // given
+        String rsaStr = "AAAB3NzaC1yc2EAAAADAQABAAABAQCvLigTfPZMqOQwHp051Co4lwwPwO21NFIXWgjQmCPEgRTqQpei7qQaxlLGkrIPjZtJQRgCuC+Sg8HFw1YpUaMybN0nFInInQLp/qe0yc9ByDZM2G86NX6W5W3+j87I8Fh1dnMov1iJ0DFVn8RLwdEGjreiZCRyJOMuHghh6y4EG7W8BwmZrse17zhSpc2wFOVhxeZnYAQFEw6g48LutFRDpoTjGgz1nz/L4zcaUxxigs8wdY+qTTOHxSTxlLqwSZPFLyYrV2KJ9mKahMuYUy6o2b8snsjvnSjyK0kY+U0C6c8fmPDFUc0RqJqfdnsIUyh11U8d3NZdaFWg0UW0SNK3";
+        // when
+        instance.decodePublicKey(rsaStr);
+    }
+
+    @Test
+    public void authorizedKeysDecoderValidDSAKey() throws GeneralSecurityException {
+        // given
+        String dsaStr = "AAAAB3NzaC1kc3MAAACBANkM1e45lxlyV24QyWBAoESlHzhYYJUfk/yUd0+Dv28okyO71DmnJesYyUzsKDpnFLlnFhxTTUGSg90fdrdubLFkRTGnHhweegMCf6kU1xyE3U6bpyMdiOXH7fOS6Q2B+qtaQRB4R5TEhdoJX648Ng+YZvLwdbZh3r/et4P46b3DAAAAFQDcu6qp67XRpzMoOS2fIL+VOxvmDwAAAIAeT3d/hbvzPoL8wV52gPtWJMU2EGoX/LJwc86Vn52NlxXB1EQSzZI50PgCKEckS80lj4GXO1ZyuBhdsBEz4rDtAIdZGW5z7WxTfcz0G2dOWmNOBqvu7j9ngfPrgtDVHYV2VL/4VpbmoPgkQLfbA9NWb6US2RnTO46rGbGurigDMQAAAIEAiI3REuOJAmgDow6HxbN0FM+RCe1JYDwJIsCRRK4JA9oYV4Pg897xqypOeXogutVu9usfcOJI6uk5OwwLqIUSaU+flgmL0LOXv4lH4+URqs7Or8+ABFTcVGGCxg0I3gwhlY2Vjc9nyHY15wqBYdUxLbe8HC6EQp9uwlLlb8LQ6a0=";
+        // when
+        PublicKey serverKey = instance.decodePublicKey(dsaStr);
+        // then
+        assertEquals(serverKey.getAlgorithm(), "DSA");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void authorizedKeysDecoderInvalidDSAKey() throws GeneralSecurityException {
+        // given
+        String dsaStr = "AAAAB3Nzakc3MAAACBANkM1e45lxlyV24QyWBAoESlHzhYYJUfk/yUd0+Dv28okyO71DmnJesYyUzsKDpnFLlnFhxTTUGSg90fdrdubLFkRTGnHhweegMCf6kU1xyE3U6bpyMdiOXH7fOS6Q2B+qtaQRB4R5TEhdoJX648Ng+YZvLwdbZh3r/et4P46b3DAAAAFQDcu6qp67XRpzMoOS2fIL+VOxvmDwAAAIAeT3d/hbvzPoL8wV52gPtWJMU2EGoX/LJwc86Vn52NlxXB1EQSzZI50PgCKEckS80lj4GXO1ZyuBhdsBEz4rDtAIdZGW5z7WxTfcz0G2dOWmNOBqvu7j9ngfPrgtDVHYV2VL/4VpbmoPgkQLfbA9NWb6US2RnTO46rGbGurigDMQAAAIEAiI3REuOJAmgDow6HxbN0FM+RCe1JYDwJIsCRRK4JA9oYV4Pg897xqypOeXogutVu9usfcOJI6uk5OwwLqIUSaU+flgmL0LOXv4lH4+URqs7Or8+ABFTcVGGCxg0I3gwhlY2Vjc9nyHY15wqBYdUxLbe8HC6EQp9uwlLlb8LQ6a0=";
+        // when
+        instance.decodePublicKey(dsaStr);
+    }
+
+    @Test
+    public void authorizedKeysDecoderValidECDSAKey() throws GeneralSecurityException {
+        // given
+        String ecdsaStr = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAP4dTrlwZmz8bZ1f901qWuFk7YelrL2WJG0jrCEAPo9UNM1wywpqjbaYUfoq+cevhLZaukDQ4N2Evux+YQ2zz0=";
+        // when
+        PublicKey serverKey = instance.decodePublicKey(ecdsaStr);
+        // then
+        assertEquals(serverKey.getAlgorithm(), "EC");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void authorizedKeysDecoderInvalidECDSAKey() throws GeneralSecurityException {
+        // given
+        String ecdsaStr = "AAAAE2VjZHNhLXNoItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAP4dTrlwZmz8bZ1f901qWuFk7YelrL2WJG0jrCEAPo9UNM1wywpqjbaYUfoq+cevhLZaukDQ4N2Evux+YQ2zz0=";
+        // when
+        instance.decodePublicKey(ecdsaStr);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void authorizedKeysDecoderInvalidKeyType() throws GeneralSecurityException {
+        // given
+        String ed25519Str = "AAAAC3NzaC1lZDI1NTE5AAAAICIvyX9C+u3KZmJ8x4DuqJg1iAKOPObCgkX9plrvu29R";
+        // when
+        instance.decodePublicKey(ed25519Str);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void decodingOfBlankInputIsCaughtAsAnError() throws GeneralSecurityException {
+        // when
+        instance.decodePublicKey("");
+   }
+}
diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeAuthorizationTest.java
new file mode 100644 (file)
index 0000000..13c5b97
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.*;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.client.session.ClientSessionImpl;
+import org.junit.Ignore;
+import org.junit.Test;
+
+
+public class CallHomeAuthorizationTest
+{
+    @Test
+    public void anAuthorizationOfRejectedIsNotAllowed() {
+        // given
+        CallHomeAuthorization auth = CallHomeAuthorization.rejected();
+        // expect
+        assertFalse(auth.isServerAllowed());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void anAuthorizationOfRejectedCannotBeAppliedToASession() {
+        // given
+        CallHomeAuthorization auth = CallHomeAuthorization.rejected();
+        // when
+        auth.applyTo(mock(ClientSession.class));
+    }
+
+    @Test
+    public void anAuthorizationOfAcceptanceIsAllowed() {
+        // given
+        String session = "some-session";
+        String user = "some-user-name";
+        ClientSessionImpl mockSession = mock(ClientSessionImpl.class);
+        doNothing().when(mockSession).setUsername(user);
+
+        // and
+        CallHomeAuthorization auth = CallHomeAuthorization.serverAccepted(session, user).build();
+        // when
+        auth.applyTo(mockSession);
+        // then
+        assertTrue(auth.isServerAllowed());
+    }
+
+    @Test
+    public void anAuthorizationOfAcceptanceCanBeAppliedToASession() {
+        // given
+        String session = "some-session";
+        String user = "some-user-name";
+        String pwd = "pwd1";
+        KeyPair pair = new KeyPair(mock(PublicKey.class), mock(PrivateKey.class));
+        ClientSessionImpl mockSession = mock(ClientSessionImpl.class);
+        doNothing().when(mockSession).setUsername(user);
+        doNothing().when(mockSession).addPasswordIdentity(pwd);
+        doNothing().when(mockSession).addPublicKeyIdentity(pair);
+        // and
+        CallHomeAuthorization auth = CallHomeAuthorization.serverAccepted(session, user)
+                .addPassword(pwd)
+                .addClientKeys(pair)
+                .build();
+        // when
+        auth.applyTo(mockSession);
+        // then
+        verify(mockSession, times(1)).addPasswordIdentity(anyString());
+        verify(mockSession, times(1)).addPublicKeyIdentity(any(KeyPair.class));
+    }
+}
diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContextTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/CallHomeSessionContextTest.java
new file mode 100644 (file)
index 0000000..3f2f468
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.security.PublicKey;
+import org.apache.sshd.ClientChannel;
+import org.apache.sshd.ClientChannel.Streaming;
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.client.channel.ChannelSubsystem;
+import org.apache.sshd.client.future.OpenFuture;
+import org.apache.sshd.client.session.ClientSessionImpl;
+import org.apache.sshd.common.KeyExchange;
+import org.apache.sshd.common.Session.AttributeKey;
+import org.apache.sshd.common.future.SshFutureListener;
+import org.apache.sshd.common.io.IoInputStream;
+import org.apache.sshd.common.io.IoOutputStream;
+import org.apache.sshd.common.io.IoReadFuture;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.util.Buffer;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.opendaylight.netconf.client.NetconfClientSessionListener;
+import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
+
+public class CallHomeSessionContextTest {
+    private ClientSessionImpl mockSession;
+    private CallHomeAuthorization mockAuth;
+    private ClientChannel mockChannel;
+    private InetSocketAddress address;
+
+    private ReverseSshChannelInitializer mockChannelInitializer;
+    private CallHomeNetconfSubsystemListener subListener;
+    private EventLoopGroup mockNettyGroup;
+    private CallHomeSessionContext.Factory realFactory;
+    private CallHomeSessionContext instance;
+    private NetconfClientSessionNegotiatorFactory mockNegotiatior;
+
+    @Before
+    public void setup() {
+        mockSession = mock(ClientSessionImpl.class);
+        mockAuth = mock(CallHomeAuthorization.class);
+        mockChannel = mock(ClientChannel.class);
+        address = mock(InetSocketAddress.class);
+
+        mockNegotiatior = mock(NetconfClientSessionNegotiatorFactory.class);
+        subListener = mock(CallHomeNetconfSubsystemListener.class);
+        mockNettyGroup = mock(EventLoopGroup.class);
+
+        realFactory = new CallHomeSessionContext.Factory(mockNettyGroup, mockNegotiatior, subListener);
+
+
+
+        KeyExchange kexMock = Mockito.mock(KeyExchange.class);
+        Mockito.doReturn(kexMock).when(mockSession).getKex();
+
+        PublicKey keyMock = Mockito.mock(PublicKey.class);
+        Mockito.doReturn(keyMock).when(kexMock).getServerKey();
+        IoReadFuture mockFuture = mock(IoReadFuture.class);
+        IoInputStream mockIn = mock(IoInputStream.class);
+        Mockito.doReturn(mockFuture).when(mockIn).read(any(Buffer.class));
+        IoOutputStream mockOut = mock(IoOutputStream.class);
+
+        Mockito.doReturn(mockIn).when(mockChannel).getAsyncOut();
+        Mockito.doReturn(mockOut).when(mockChannel).getAsyncIn();
+
+        Mockito.doReturn(true).when(mockAuth).isServerAllowed();
+
+        IoSession ioSession = mock(IoSession.class);
+        Mockito.doReturn(ioSession).when(mockSession).getIoSession();
+        Mockito.doReturn(address).when(ioSession).getRemoteAddress();
+        Mockito.doReturn(null).when(mockSession).setAttribute(any(AttributeKey.class), any());
+        Mockito.doReturn(null).when(mockSession).getAttribute(any(AttributeKey.class));
+        Mockito.doReturn("testSession").when(mockSession).toString();
+
+        Mockito.doNothing().when(mockAuth).applyTo(mockSession);
+        Mockito.doReturn("test").when(mockAuth).getSessionName();
+    }
+
+    @Test
+    public void theContextShouldBeSettableAndRetrievableAsASessionAttribute() {
+        // redo instance below because previous constructor happened too early to capture behavior
+        instance = realFactory.createIfNotExists(mockSession, mockAuth, address);
+        // when
+        CallHomeSessionContext.getFrom(mockSession);
+        // then
+        verify(mockSession, times(1)).setAttribute(CallHomeSessionContext.SESSION_KEY, instance);
+        verify(mockSession, times(1)).getAttribute(CallHomeSessionContext.SESSION_KEY);
+    }
+
+    @Test
+    public void anAuthorizeActionShouldApplyToTheBoundSession() throws IOException {
+        instance = realFactory.createIfNotExists(mockSession, mockAuth, address);
+        // when
+        Mockito.doReturn(null).when(mockSession).auth();
+        instance.authorize();
+        // then
+        verify(mockAuth, times(1)).applyTo(mockSession);
+    }
+
+    @Test
+    public void creatingAChannelSuccessfullyShouldResultInAnAttachedListener() throws IOException {
+        // given
+        OpenFuture mockFuture = mock(OpenFuture.class);
+        ChannelSubsystem mockChannel = mock(ChannelSubsystem.class);
+        Mockito.doReturn(mockFuture).when(mockChannel).open();
+        Mockito.doReturn(mockChannel).when(mockSession).createSubsystemChannel(anyString());
+
+        Mockito.doReturn(null).when(mockFuture).addListener(any(SshFutureListener.class));
+        Mockito.doNothing().when(mockChannel).setStreaming(any(Streaming.class));
+        instance = realFactory.createIfNotExists(mockSession, mockAuth, address);
+        // when
+        instance.openNetconfChannel();
+        // then
+        verify(mockFuture, times(1)).addListener(any(SshFutureListener.class));
+    }
+
+    static class TestableContext extends CallHomeSessionContext {
+        MinaSshNettyChannel minaMock;
+
+        public TestableContext(ClientSession sshSession, CallHomeAuthorization authorization, InetSocketAddress address,
+                CallHomeSessionContext.Factory factory, MinaSshNettyChannel minaMock) {
+            super(sshSession, authorization, address, factory);
+            this.minaMock = minaMock;
+        }
+
+        @Override
+        protected MinaSshNettyChannel newMinaSshNettyChannel(ClientChannel netconfChannel) {
+            return minaMock;
+        }
+    }
+
+    @Ignore
+    @Test
+    public void openingTheChannelSuccessfullyShouldFireActiveChannel() {
+        // given
+        MinaSshNettyChannel mockMinaChannel = mock(MinaSshNettyChannel.class);
+        CallHomeSessionContext.Factory mockFactory = mock(CallHomeSessionContext.Factory.class);
+
+        ChannelFuture mockChanFuture = mock(ChannelFuture.class);
+        Mockito.doReturn(mockChanFuture).when(mockNettyGroup).register(any(Channel.class));
+
+        Mockito.doReturn(mockNettyGroup).when(mockFactory).getNettyGroup();
+        Mockito.doReturn(mockChannelInitializer).when(mockFactory)
+                .getChannelInitializer(any(NetconfClientSessionListener.class));
+
+        ChannelPipeline mockPipeline = mock(ChannelPipeline.class);
+        Mockito.doReturn(mockPipeline).when(mockMinaChannel).pipeline();
+
+        OpenFuture mockFuture = mock(OpenFuture.class);
+        Mockito.doReturn(true).when(mockFuture).isOpened();
+
+        instance = new TestableContext(mockSession, mockAuth, address, mockFactory, mockMinaChannel);
+        SshFutureListener<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());
+    }
+}
diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannelTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/MinaSshNettyChannelTest.java
new file mode 100644 (file)
index 0000000..17193a2
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.EmptyByteBuf;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import org.apache.sshd.ClientChannel;
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.common.future.SshFutureListener;
+import org.apache.sshd.common.io.IoInputStream;
+import org.apache.sshd.common.io.IoOutputStream;
+import org.apache.sshd.common.io.IoReadFuture;
+import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.util.Buffer;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+
+public class MinaSshNettyChannelTest {
+    CallHomeSessionContext mockContext;
+    ClientSession mockSession;
+    ClientChannel mockChannel;
+    MinaSshNettyChannel instance;
+
+    @Before
+    public void setup() {
+        IoReadFuture mockFuture = mock(IoReadFuture.class);
+        IoInputStream mockIn = mock(IoInputStream.class);
+        Mockito.doReturn(mockFuture).when(mockIn).read(any(Buffer.class));
+        IoOutputStream mockOut = mock(IoOutputStream.class);
+        mockContext = mock(CallHomeSessionContext.class);
+        mockSession = mock(ClientSession.class);
+        mockChannel = mock(ClientChannel.class);
+        Mockito.doReturn(mockIn).when(mockChannel).getAsyncOut();
+        Mockito.doReturn(mockOut).when(mockChannel).getAsyncIn();
+
+        IoWriteFuture mockWrFuture = mock(IoWriteFuture.class);
+        Mockito.doReturn(false).when(mockOut).isClosed();
+        Mockito.doReturn(false).when(mockOut).isClosing();
+        Mockito.doReturn(mockWrFuture).when(mockOut).write(any(Buffer.class));
+        Mockito.doReturn(null).when(mockWrFuture).addListener(any());
+
+        Mockito.doReturn(mockFuture).when(mockFuture).addListener(Mockito.any());
+
+        instance = new MinaSshNettyChannel(mockContext, mockSession, mockChannel);
+    }
+
+    @Test
+    public void ourChannelHandlerShouldBeFirstInThePipeline() {
+        // given
+        ChannelHandler firstHandler = instance.pipeline().first();
+        String firstName = firstHandler.getClass().getName();
+        // expect
+        assertTrue(firstName.contains("callhome"));
+    }
+
+    @Test
+    public void ourChannelHandlerShouldForwardWrites() throws Exception {
+        ChannelHandler mockHandler = mock(ChannelHandler.class);
+        ChannelHandlerContext ctx = mock(ChannelHandlerContext.class);
+        Mockito.doReturn(mockHandler).when(ctx).handler();
+        ChannelPromise promise = mock(ChannelPromise.class);
+
+        ByteBufAllocator mockAlloc = mock(ByteBufAllocator.class);
+        ByteBuf bytes = new EmptyByteBuf(mockAlloc);
+
+        // we would really like to just verify that the async handler write() was
+        // called but it is a final class, so no mocking. instead we set up the
+        // mock channel to have no async input, which will cause a failure later
+        // on the write promise that we use as a cheap way to tell that write()
+        // got called. ick.
+
+        Mockito.doReturn(null).when(mockChannel).getAsyncIn();
+        Mockito.doReturn(null).when(promise).setFailure(any(Throwable.class));
+
+        // Need to reconstruct instance to pick up null async in above
+        instance = new MinaSshNettyChannel(mockContext, mockSession, mockChannel);
+
+        // when
+        ChannelOutboundHandlerAdapter outadapter = (ChannelOutboundHandlerAdapter) instance.pipeline().first();
+        outadapter.write(ctx, bytes, promise);
+
+        // then
+        verify(promise, times(1)).setFailure(any(Throwable.class));
+    }
+}
diff --git a/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerTest.java b/netconf/callhome-protocol/src/test/java/org/opendaylight/netconf/callhome/protocol/NetconfCallHomeServerTest.java
new file mode 100644 (file)
index 0000000..2530217
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.protocol;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.PublicKey;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.SshClient;
+import org.apache.sshd.client.future.AuthFuture;
+import org.apache.sshd.client.session.ClientSessionImpl;
+import org.apache.sshd.common.Session;
+import org.apache.sshd.common.SessionListener;
+import org.apache.sshd.common.future.SshFutureListener;
+import org.apache.sshd.common.io.IoAcceptor;
+import org.apache.sshd.common.io.IoHandler;
+import org.apache.sshd.common.io.IoServiceFactory;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NetconfCallHomeServerTest {
+
+    SshClient mockSshClient;
+    CallHomeAuthorizationProvider mockCallHomeAuthProv;
+    CallHomeAuthorization mockAuth;
+    CallHomeSessionContext.Factory mockFactory;
+    InetSocketAddress mockAddress;
+    ClientSession mockSession;
+
+    NetconfCallHomeServer instance;
+
+    @Before
+    public void setup() {
+        mockSshClient = Mockito.spy(SshClient.setUpDefaultClient());
+        mockCallHomeAuthProv = mock(CallHomeAuthorizationProvider.class);
+        mockAuth = mock(CallHomeAuthorization.class);
+        mockFactory = mock(CallHomeSessionContext.Factory.class);
+        mockAddress = InetSocketAddress.createUnresolved("1.2.3.4", 123);
+        mockSession = mock(ClientSession.class);
+
+        Map<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);
+    }
+
+}
diff --git a/netconf/callhome-provider/pom.xml b/netconf/callhome-provider/pom.xml
new file mode 100644 (file)
index 0000000..f2a4ef9
--- /dev/null
@@ -0,0 +1,60 @@
+<?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>
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/BaseCallHomeTopology.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/BaseCallHomeTopology.java
new file mode 100644 (file)
index 0000000..b7861a3
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import io.netty.util.concurrent.EventExecutor;
+import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
+import org.opendaylight.controller.config.threadpool.ThreadPool;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService;
+import org.opendaylight.controller.sal.binding.api.BindingAwareBroker;
+import org.opendaylight.controller.sal.core.api.Broker;
+import org.opendaylight.netconf.client.NetconfClientDispatcher;
+import org.opendaylight.netconf.topology.AbstractNetconfTopology;
+import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider;
+
+abstract class BaseCallHomeTopology extends AbstractNetconfTopology {
+
+    protected DOMMountPointService mountPointService = null;
+
+    protected BaseCallHomeTopology(String topologyId, NetconfClientDispatcher clientDispatcher,
+            BindingAwareBroker bindingAwareBroker, Broker domBroker, EventExecutor eventExecutor,
+            ScheduledThreadPool keepaliveExecutor, ThreadPool processingExecutor,
+            SchemaRepositoryProvider schemaRepositoryProvider, DataBroker dataBroker, DOMMountPointService mountPointService) {
+        super(topologyId, clientDispatcher, bindingAwareBroker, domBroker, eventExecutor, keepaliveExecutor,
+                processingExecutor, schemaRepositoryProvider, dataBroker);
+        this.mountPointService = mountPointService;
+    }
+
+}
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeAuthProviderImpl.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeAuthProviderImpl.java
new file mode 100644 (file)
index 0000000..485df12
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.callhome.mount;
+
+import com.google.common.base.Objects;
+import com.google.common.net.InetAddresses;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.PublicKey;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeChangeListener;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
+import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.netconf.callhome.protocol.AuthorizedKeysDecoder;
+import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorization;
+import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorization.Builder;
+import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorizationProvider;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.NetconfCallhomeServer;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.credentials.Credentials;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.AllowedDevices;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.Global;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev161109.netconf.callhome.server.allowed.devices.Device;
+import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CallHomeAuthProviderImpl implements CallHomeAuthorizationProvider, AutoCloseable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(CallHomeAuthProviderImpl.class);
+    private static final InstanceIdentifier<Global> 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;
+        }
+
+    }
+
+}
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcher.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcher.java
new file mode 100644 (file)
index 0000000..807b7a1
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.FailedFuture;
+import io.netty.util.concurrent.Future;
+import java.net.InetSocketAddress;
+import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
+import org.opendaylight.controller.config.threadpool.ThreadPool;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService;
+import org.opendaylight.controller.sal.binding.api.BindingAwareBroker;
+import org.opendaylight.controller.sal.core.api.Broker;
+import org.opendaylight.netconf.callhome.mount.CallHomeMountSessionContext.CloseCallback;
+import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator;
+import org.opendaylight.netconf.callhome.protocol.CallHomeNetconfSubsystemListener;
+import org.opendaylight.netconf.callhome.protocol.CallHomeProtocolSessionContext;
+import org.opendaylight.netconf.client.NetconfClientDispatcher;
+import org.opendaylight.netconf.client.NetconfClientSession;
+import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
+import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfiguration;
+import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class CallHomeMountDispatcher implements NetconfClientDispatcher, CallHomeNetconfSubsystemListener {
+
+    private final static Logger LOG = LoggerFactory.getLogger(CallHomeMountDispatcher.class);
+
+    private final String topologyId;
+    private final BindingAwareBroker bindingAwareBroker;
+    private final EventExecutor eventExecutor;
+    private final ScheduledThreadPool keepaliveExecutor;
+    private final ThreadPool processingExecutor;
+    private final SchemaRepositoryProvider schemaRepositoryProvider;
+    private final org.opendaylight.controller.sal.core.api.Broker domBroker;
+    private final CallHomeMountSessionManager sessionManager;
+    private final DataBroker dataBroker;
+    private final DOMMountPointService mountService;
+
+
+    private CallHomeTopology topology;
+
+    private final CloseCallback onCloseHandler = new CloseCallback() {
+
+        @Override
+        public void onClosed(CallHomeMountSessionContext deviceContext) {
+            LOG.info("Removing {} from Netconf Topology.", deviceContext.getId());
+            topology.disconnectNode(deviceContext.getId());
+        }
+    };
+
+
+    public CallHomeMountDispatcher(String topologyId, BindingAwareBroker bindingAwareBroker,
+            EventExecutor eventExecutor, ScheduledThreadPool keepaliveExecutor, ThreadPool processingExecutor,
+            SchemaRepositoryProvider schemaRepositoryProvider, Broker domBroker, DataBroker dataBroker, DOMMountPointService mountService) {
+        this.topologyId = topologyId;
+        this.bindingAwareBroker = bindingAwareBroker;
+        this.eventExecutor = eventExecutor;
+        this.keepaliveExecutor = keepaliveExecutor;
+        this.processingExecutor = processingExecutor;
+        this.schemaRepositoryProvider = schemaRepositoryProvider;
+        this.domBroker = domBroker;
+        this.sessionManager = new CallHomeMountSessionManager();
+        this.dataBroker = dataBroker;
+        this.mountService = mountService;
+    }
+
+
+    @Override
+    public Future<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;
+    }
+}
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContext.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContext.java
new file mode 100644 (file)
index 0000000..42d6aab
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import com.google.common.base.Preconditions;
+import io.netty.util.concurrent.Promise;
+import java.net.InetSocketAddress;
+import java.security.PublicKey;
+import org.opendaylight.netconf.api.NetconfMessage;
+import org.opendaylight.netconf.api.NetconfTerminationReason;
+import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator;
+import org.opendaylight.netconf.callhome.protocol.CallHomeProtocolSessionContext;
+import org.opendaylight.netconf.client.NetconfClientSession;
+import org.opendaylight.netconf.client.NetconfClientSessionListener;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNodeBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.netconf.node.credentials.credentials.LoginPasswordBuilder;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeBuilder;
+
+class CallHomeMountSessionContext {
+
+    public interface CloseCallback {
+
+        void onClosed(CallHomeMountSessionContext deviceContext);
+
+    }
+
+    private final NodeId nodeId;
+    private final CallHomeChannelActivator activator;
+    private final CallHomeProtocolSessionContext protocol;
+    private final CloseCallback onClose;
+    // FIXME: Remove this
+    private final ContextKey key;
+
+    CallHomeMountSessionContext(String nodeId, CallHomeProtocolSessionContext protocol,
+            CallHomeChannelActivator activator, CloseCallback callback) {
+
+        this.nodeId = new NodeId(Preconditions.checkNotNull(nodeId, "nodeId"));
+        this.key = ContextKey.from(protocol.getRemoteAddress());
+        this.protocol = Preconditions.checkNotNull(protocol, "protocol");
+        this.activator = Preconditions.checkNotNull(activator, "activator");
+        this.onClose = Preconditions.checkNotNull(callback, "callback");
+    }
+
+    NodeId getId() {
+        return nodeId;
+    }
+
+    public ContextKey getContextKey() {
+        return key;
+    }
+
+    Node getConfigNode() {
+        NodeBuilder builder = new NodeBuilder();
+
+        return builder.setNodeId(getId()).addAugmentation(NetconfNode.class, configNetconfNode()).build();
+
+    }
+
+    private NetconfNode configNetconfNode() {
+        NetconfNodeBuilder node = new NetconfNodeBuilder();
+        node.setHost(new Host(key.getIpAddress()));
+        node.setPort(key.getPort());
+        node.setTcpOnly(Boolean.FALSE);
+        node.setCredentials(new LoginPasswordBuilder().setUsername("ommited").setPassword("ommited").build());
+        node.setSchemaless(Boolean.FALSE);
+        return node.build();
+    }
+
+    @SuppressWarnings("unchecked")
+     <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();
+    }
+
+}
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionManager.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionManager.java
new file mode 100644 (file)
index 0000000..7f6187d
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.PublicKey;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import javax.annotation.Nullable;
+import org.opendaylight.netconf.callhome.mount.CallHomeMountSessionContext.CloseCallback;
+import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator;
+import org.opendaylight.netconf.callhome.protocol.CallHomeProtocolSessionContext;
+
+public class CallHomeMountSessionManager implements CallHomeMountSessionContext.CloseCallback {
+
+    private final ConcurrentMap<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);
+    }
+
+}
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeTopology.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/CallHomeTopology.java
new file mode 100644 (file)
index 0000000..bdacf1d
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import io.netty.util.concurrent.EventExecutor;
+import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
+import org.opendaylight.controller.config.threadpool.ThreadPool;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService;
+import org.opendaylight.controller.sal.binding.api.BindingAwareBroker;
+import org.opendaylight.controller.sal.core.api.Broker;
+import org.opendaylight.netconf.client.NetconfClientDispatcher;
+import org.opendaylight.netconf.sal.connect.api.RemoteDeviceHandler;
+import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfSessionPreferences;
+import org.opendaylight.netconf.sal.connect.netconf.sal.NetconfDeviceSalFacade;
+import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId;
+import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider;
+
+
+public class CallHomeTopology extends BaseCallHomeTopology {
+
+    public CallHomeTopology(String topologyId, NetconfClientDispatcher clientDispatcher,
+            BindingAwareBroker bindingAwareBroker, Broker domBroker, EventExecutor eventExecutor,
+            ScheduledThreadPool keepaliveExecutor, ThreadPool processingExecutor,
+            SchemaRepositoryProvider schemaRepositoryProvider,final DataBroker dataBroker, final DOMMountPointService mountPointService) {
+        super(topologyId, clientDispatcher, bindingAwareBroker, domBroker, eventExecutor, keepaliveExecutor, processingExecutor,
+                schemaRepositoryProvider, dataBroker, mountPointService);
+    }
+
+    @Override
+    protected RemoteDeviceHandler<NetconfSessionPreferences> createSalFacade(RemoteDeviceId id, Broker domBroker,
+            BindingAwareBroker bindingBroker) {
+        return new NetconfDeviceSalFacade(id, domBroker, bindingAwareBroker);
+    }
+}
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/Configuration.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/Configuration.java
new file mode 100644 (file)
index 0000000..f115b86
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class Configuration {
+    public abstract static class ConfigurationException extends RuntimeException {
+        ConfigurationException(String msg) {
+            super(msg);
+        }
+
+        ConfigurationException(String msg, Exception cause) {
+            super(msg, cause);
+        }
+    }
+
+    public static class ReadException extends ConfigurationException {
+        ReadException(String msg, Exception e) {
+            super(msg, e);
+        }
+    }
+
+    public static class MissingException extends ConfigurationException {
+        private final String key;
+
+        MissingException(String key) {
+            super("Key not found: " + key);
+            this.key = key;
+        }
+
+        public String getKey() {
+            return key;
+        }
+    }
+
+    public static class IllegalValueException extends ConfigurationException {
+        private final String key;
+        private final String value;
+
+        IllegalValueException(String key, String value) {
+            super("Key has an illegal value. Key: " + key + ", Value: " + value);
+            this.key = key;
+            this.value = value;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    private String path;
+    private Properties properties;
+
+    public Configuration(String path) throws ConfigurationException {
+        this.path = path;
+        try {
+            this.properties = readFromPath(path);
+        } catch (IOException ioe) {
+            throw new ReadException(path, ioe);
+        }
+    }
+
+    private Properties readFromPath(String path) throws IOException {
+        return readFromFile(new File(path));
+    }
+
+    private Properties readFromFile(File file) throws IOException {
+        FileInputStream stream = new FileInputStream(file);
+        properties = readFrom(stream);
+        return properties;
+    }
+
+    private Properties readFrom(InputStream stream) throws IOException {
+        Properties properties = new Properties();
+        properties.load(stream);
+        return properties;
+    }
+
+    public String get(String key) {
+        String result = (String) properties.get(key);
+        if (result == null)
+            throw new MissingException(key);
+        return result;
+    }
+
+    public int getAsPort(String key) {
+        String s = get(key);
+        try {
+            int newPort = Integer.parseInt(s);
+            if (newPort < 0 || newPort > 65535)
+                throw new IllegalValueException(key, s);
+            return newPort;
+        } catch (NumberFormatException e) {
+            throw new IllegalValueException(key, s);
+        }
+    }
+}
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/ContextKey.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/ContextKey.java
new file mode 100644 (file)
index 0000000..cb0578f
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode;
+
+class ContextKey {
+
+    private final IpAddress address;
+    private final PortNumber port;
+
+    public ContextKey(IpAddress address, PortNumber port) {
+        this.address = Preconditions.checkNotNull(address);
+        this.port = Preconditions.checkNotNull(port);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + address.hashCode();
+        result = prime * result + port.hashCode();
+        return result;
+    }
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ContextKey other = (ContextKey) obj;
+        return Objects.equal(address, other.address) && Objects.equal(port, other.port);
+    }
+
+    public static ContextKey from(NetconfNode node) {
+        return new ContextKey(node.getHost().getIpAddress(), node.getPort());
+    }
+
+    IpAddress getIpAddress() {
+        return address;
+    }
+
+    PortNumber getPort() {
+        return port;
+    }
+
+    public static ContextKey from(SocketAddress remoteAddress) {
+        Preconditions.checkArgument(remoteAddress instanceof InetSocketAddress);
+        InetSocketAddress inetSocketAddr = (InetSocketAddress) remoteAddress;
+        InetAddress ipAddress = inetSocketAddr.getAddress();
+
+        final IpAddress yangIp;
+        if(ipAddress instanceof Inet4Address) {
+            yangIp = new IpAddress(IetfInetUtil.INSTANCE.ipv4AddressFor(ipAddress));
+        } else {
+            Preconditions.checkArgument(ipAddress instanceof Inet6Address);
+            yangIp = new IpAddress(IetfInetUtil.INSTANCE.ipv6AddressFor(ipAddress));
+        }
+        return new ContextKey(yangIp, new PortNumber(inetSocketAddr.getPort()));
+    }
+
+    @Override
+    public String toString() {
+        if(address.getIpv4Address() != null) {
+            return address.getIpv4Address().getValue() + ":" + port.getValue();
+        }
+        return address.getIpv6Address().getValue() + ":" + port.getValue();
+    }
+}
\ No newline at end of file
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/IetfZeroTouchCallHomeServerProvider.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/IetfZeroTouchCallHomeServerProvider.java
new file mode 100644 (file)
index 0000000..2637335
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import java.io.IOException;
+import java.io.File;
+import java.net.InetSocketAddress;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.netconf.callhome.protocol.CallHomeAuthorizationProvider;
+import org.opendaylight.netconf.callhome.protocol.NetconfCallHomeServer;
+import org.opendaylight.netconf.callhome.protocol.NetconfCallHomeServerBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class IetfZeroTouchCallHomeServerProvider implements AutoCloseable {
+    private static final Logger LOG = LoggerFactory.getLogger(IetfZeroTouchCallHomeServerProvider.class);
+    private static final String appName = "CallHomeServer";
+
+    private final DataBroker dataBroker;
+    private final CallHomeMountDispatcher mountDispacher;
+
+    protected NetconfCallHomeServer server;
+
+
+    private static final String CALL_HOME_PORT_KEY = "DefaultCallHomePort";
+
+    private final CallHomeAuthProviderImpl authProvider;
+
+
+    static String configurationPath = "etc"+File.pathSeparator+"ztp-callhome-config.cfg";
+
+    private int port = 0; // 0 = use default in NetconfCallHomeBuilder
+
+    public IetfZeroTouchCallHomeServerProvider(DataBroker dataBroker, CallHomeMountDispatcher mountDispacher) {
+        this.dataBroker = dataBroker;
+        this.mountDispacher = mountDispacher;
+        this.authProvider = new CallHomeAuthProviderImpl(dataBroker);
+    }
+
+    public void init() {
+        // Register itself as a listener to changes in Devices subtree
+        try {
+            LOG.info("Initializing provider for {}", appName);
+            loadConfigurableValues(configurationPath);
+            initializeServer();
+            LOG.info("Initialization complete for {}", appName);
+        } catch (IndexOutOfBoundsException | Configuration.ConfigurationException e) {
+            LOG.error("Unable to successfully initialize", e);
+        }
+    }
+
+    void loadConfigurableValues(String configurationPath) throws Configuration.ConfigurationException {
+        try {
+            Configuration configuration = new Configuration(configurationPath);
+            port = configuration.getAsPort(CALL_HOME_PORT_KEY);
+        } catch (Exception e) {
+            LOG.error("Problem trying to load configuration values from {}", configurationPath, e);
+        }
+    }
+
+    private CallHomeAuthorizationProvider getCallHomeAuthorization() {
+        return authProvider;
+    }
+
+    private void initializeServer() {
+        LOG.info("Initializing Call Home server instance");
+        CallHomeAuthorizationProvider auth =  getCallHomeAuthorization();
+        NetconfCallHomeServerBuilder builder = new NetconfCallHomeServerBuilder(auth, mountDispacher);
+
+        if (port > 0)
+            builder.setBindAddress(new InetSocketAddress(port));
+        server = builder.build();
+        try {
+            server.bind();
+            mountDispacher.createTopology();
+            LOG.info("Initialization complete for Call Home server instance");
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to create Call Home Server.",e);
+        }
+    }
+
+    @Override
+    public void close() throws Exception {
+        authProvider.close();
+        if (server != null) {
+            server.close();
+        }
+
+        LOG.info("Successfully closed provider for {}", appName);
+    }
+
+}
diff --git a/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/SchemaRepositoryProviderImpl.java b/netconf/callhome-provider/src/main/java/org/opendaylight/netconf/callhome/mount/SchemaRepositoryProviderImpl.java
new file mode 100644 (file)
index 0000000..331ea85
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.callhome.mount;
+
+import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider;
+import org.opendaylight.yangtools.yang.parser.repo.SharedSchemaRepository;
+
+// FIXME: Figure out why blueprint rejects to instantiate if class is not public
+public class SchemaRepositoryProviderImpl implements SchemaRepositoryProvider {
+
+    private final SharedSchemaRepository schemaRepository;
+
+    public SchemaRepositoryProviderImpl(final String moduleName) {
+        schemaRepository = new SharedSchemaRepository(moduleName);
+    }
+
+    @Override
+    public SharedSchemaRepository getSharedSchemaRepository() {
+        return schemaRepository;
+    }
+}
\ No newline at end of file
diff --git a/netconf/callhome-provider/src/main/resources/org/opendaylight/blueprint/callhome-topology.xml b/netconf/callhome-provider/src/main/resources/org/opendaylight/blueprint/callhome-topology.xml
new file mode 100755 (executable)
index 0000000..657a481
--- /dev/null
@@ -0,0 +1,55 @@
+<?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
diff --git a/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcherTest.java b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountDispatcherTest.java
new file mode 100644 (file)
index 0000000..fc59e27
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Future;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import org.junit.Before;
+import org.junit.Test;
+import org.opendaylight.controller.config.threadpool.ScheduledThreadPool;
+import org.opendaylight.controller.config.threadpool.ThreadPool;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.dom.api.DOMMountPointService;
+import org.opendaylight.controller.sal.binding.api.BindingAwareBroker;
+import org.opendaylight.controller.sal.core.api.Broker;
+import org.opendaylight.netconf.api.messages.NetconfHelloMessageAdditionalHeader;
+import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator;
+import org.opendaylight.netconf.client.NetconfClientSession;
+import org.opendaylight.netconf.client.NetconfClientSessionListener;
+import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
+import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder;
+import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.AuthenticationHandler;
+import org.opendaylight.netconf.topology.api.SchemaRepositoryProvider;
+import org.opendaylight.protocol.framework.ReconnectStrategy;
+
+public class CallHomeMountDispatcherTest {
+    private String topologyId;
+    private BindingAwareBroker mockBroker;
+    private EventExecutor mockExecutor;
+    private ScheduledThreadPool mockKeepAlive;
+    private ThreadPool mockProcessingExecutor;
+    private SchemaRepositoryProvider mockSchemaRepoProvider;
+    private Broker mockDomBroker;
+
+    private CallHomeMountDispatcher instance;
+    private DataBroker mockDataBroker;
+    private DOMMountPointService mockMount;
+
+    @Before
+    public void setup() {
+        topologyId = "";
+        mockBroker = mock(BindingAwareBroker.class);
+        mockExecutor = mock(EventExecutor.class);
+        mockKeepAlive = mock(ScheduledThreadPool.class);
+        mockProcessingExecutor = mock(ThreadPool.class);
+        mockSchemaRepoProvider = mock(SchemaRepositoryProvider.class);
+        mockDomBroker = mock(Broker.class);
+        mockDataBroker = mock(DataBroker.class);
+        mockMount = mock(DOMMountPointService.class);
+        instance = new CallHomeMountDispatcher(topologyId, mockBroker, mockExecutor, mockKeepAlive,
+                mockProcessingExecutor, mockSchemaRepoProvider, mockDomBroker, mockDataBroker, mockMount);
+    }
+
+    NetconfClientConfiguration someConfiguration(InetSocketAddress address) {
+        // NetconfClientConfiguration has mostly final methods, making it un-mock-able
+
+        NetconfClientConfiguration.NetconfClientProtocol protocol =
+                NetconfClientConfiguration.NetconfClientProtocol.SSH;
+        NetconfHelloMessageAdditionalHeader additionalHeader = mock(NetconfHelloMessageAdditionalHeader.class);
+        NetconfClientSessionListener sessionListener = mock(NetconfClientSessionListener.class);
+        ReconnectStrategy reconnectStrategy = mock(ReconnectStrategy.class);
+        AuthenticationHandler authHandler = mock(AuthenticationHandler.class);
+
+        return NetconfClientConfigurationBuilder.create().withProtocol(protocol).withAddress(address)
+                .withConnectionTimeoutMillis(0).withAdditionalHeader(additionalHeader)
+                .withSessionListener(sessionListener).withReconnectStrategy(reconnectStrategy)
+                .withAuthHandler(authHandler).build();
+    }
+
+    @Test
+    public void canCreateASessionFromAConfiguration() {
+        // given
+        CallHomeMountSessionContext mockContext = mock(CallHomeMountSessionContext.class);
+        InetSocketAddress someAddress = InetSocketAddress.createUnresolved("1.2.3.4", 123);
+        // instance.contextByAddress.put(someAddress, mockContext);
+
+        NetconfClientConfiguration someCfg = someConfiguration(someAddress);
+        // when
+        instance.createClient(someCfg);
+        // then
+        // verify(mockContext, times(1)).activate(any(NetconfClientSessionListener.class));
+    }
+
+    @Test
+    public void noSessionIsCreatedWithoutAContextAvailableForAGivenAddress() {
+        // given
+        InetSocketAddress someAddress = InetSocketAddress.createUnresolved("1.2.3.4", 123);
+        NetconfClientConfiguration someCfg = someConfiguration(someAddress);
+        // when
+        Future<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));
+    }
+}
diff --git a/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContextTest.java b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/CallHomeMountSessionContextTest.java
new file mode 100644 (file)
index 0000000..c91f7a8
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.netty.util.concurrent.Promise;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.opendaylight.netconf.api.NetconfMessage;
+import org.opendaylight.netconf.api.NetconfTerminationReason;
+import org.opendaylight.netconf.callhome.protocol.CallHomeChannelActivator;
+import org.opendaylight.netconf.callhome.protocol.CallHomeProtocolSessionContext;
+import org.opendaylight.netconf.client.NetconfClientSession;
+import org.opendaylight.netconf.client.NetconfClientSessionListener;
+
+
+public class CallHomeMountSessionContextTest {
+    private Inet4Address someAddressIpv4;
+    private InetSocketAddress someSocketAddress;
+    private CallHomeChannelActivator mockActivator;
+    private CallHomeMountSessionContext.CloseCallback mockCallback;
+    private CallHomeMountSessionContext instance;
+    private CallHomeProtocolSessionContext mockProtocol;
+
+    @Before
+    public void setup() throws UnknownHostException {
+        someAddressIpv4 = (Inet4Address) InetAddress.getByName("1.2.3.4");
+        someSocketAddress = new InetSocketAddress(someAddressIpv4, 123);
+
+        mockProtocol = mock(CallHomeProtocolSessionContext.class);
+        mockActivator = mock(CallHomeChannelActivator.class);
+        mockCallback = mock(CallHomeMountSessionContext.CloseCallback.class);
+        doReturn(someSocketAddress).when(mockProtocol).getRemoteAddress();
+
+        instance = new CallHomeMountSessionContext("test",mockProtocol, mockActivator, mockCallback);
+    }
+
+    @Test
+    public void configNodeCanBeCreated() {
+        assertNotNull(instance.getConfigNode());
+    }
+
+    @Test
+    public void activationOfListenerSupportsSessionUp() {
+        // given
+        when(mockActivator.activate(any(NetconfClientSessionListener.class)))
+                .thenAnswer(invocationOnMock -> {
+                        NetconfClientSession mockSession = mock(NetconfClientSession.class);
+
+                        Object arg = invocationOnMock.getArguments()[0];
+                        ((NetconfClientSessionListener) arg).onSessionUp(mockSession);
+                        return null;
+                    });
+
+        NetconfClientSessionListener mockListener = mock(NetconfClientSessionListener.class);
+        // when
+        mockActivator.activate(mockListener);
+        // then
+        verify(mockListener, times(1)).onSessionUp(any(NetconfClientSession.class));
+    }
+
+    @Test
+    public void activationOfListenerSupportsSessionTermination() {
+        // given
+        when(mockActivator.activate(any(NetconfClientSessionListener.class)))
+                .thenAnswer(invocationOnMock -> {
+                        NetconfClientSession mockSession = mock(NetconfClientSession.class);
+                        NetconfTerminationReason mockReason = mock(NetconfTerminationReason.class);
+
+                        Object arg = invocationOnMock.getArguments()[0];
+                        ((NetconfClientSessionListener) arg).onSessionTerminated(mockSession, mockReason);
+                        return null;
+                    });
+
+        NetconfClientSessionListener mockListener = mock(NetconfClientSessionListener.class);
+        // when
+        mockActivator.activate(mockListener);
+        // then
+        verify(mockListener, times(1)).onSessionTerminated(any(NetconfClientSession.class),
+                any(NetconfTerminationReason.class));
+    }
+
+    @Test
+    public void activationOfListenerSupportsSessionDown() {
+        // given
+        when(mockActivator.activate(any(NetconfClientSessionListener.class)))
+                .thenAnswer(invocationOnMock -> {
+                        NetconfClientSession mockSession = mock(NetconfClientSession.class);
+                        Exception mockException = mock(Exception.class);
+
+                        Object arg = invocationOnMock.getArguments()[0];
+                        ((NetconfClientSessionListener) arg).onSessionDown(mockSession, mockException);
+                        return null;
+                    });
+        // given
+        NetconfClientSessionListener mockListener = mock(NetconfClientSessionListener.class);
+        // when
+        mockActivator.activate(mockListener);
+        // then
+        verify(mockListener, times(1)).onSessionDown(any(NetconfClientSession.class), any(Exception.class));
+    }
+
+    @Test
+    public void activationOfListenerSupportsSessionMessages() {
+        // given
+        when(mockActivator.activate(any(NetconfClientSessionListener.class)))
+                .thenAnswer(invocationOnMock -> {
+                        NetconfClientSession mockSession = mock(NetconfClientSession.class);
+                        NetconfMessage mockMsg = mock(NetconfMessage.class);
+
+                        Object arg = invocationOnMock.getArguments()[0];
+                        ((NetconfClientSessionListener) arg).onMessage(mockSession, mockMsg);
+                        return null;
+                    });
+        // given
+        NetconfClientSessionListener mockListener = mock(NetconfClientSessionListener.class);
+        // when
+        mockActivator.activate(mockListener);
+        // then
+        verify(mockListener, times(1)).onMessage(any(NetconfClientSession.class), any(NetconfMessage.class));
+    }
+}
diff --git a/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/ContextKeyTest.java b/netconf/callhome-provider/src/test/java/org/opendaylight/netconf/callhome/mount/ContextKeyTest.java
new file mode 100644 (file)
index 0000000..e44ed22
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2016 Brocade Communication Systems and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.callhome.mount;
+
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import org.junit.Before;
+import org.junit.Test;
+import org.opendaylight.netconf.client.NetconfClientSession;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddressBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode;
+
+
+public class ContextKeyTest {
+    IpAddress address1;
+    IpAddress address2;
+
+    PortNumber port1;
+    PortNumber port2;
+
+    NetconfNode mockNode;
+    NetconfClientSession mockSession;
+
+    ContextKey instance1;
+    ContextKey instance2;
+    ContextKey instance3;
+    ContextKey instance4;
+
+    @Before
+    public void setup() {
+        address1 = IpAddressBuilder.getDefaultInstance("1.2.3.4");
+        address2 = IpAddressBuilder.getDefaultInstance("5.6.7.8");
+
+        port1 = new PortNumber(123);
+        port2 = new PortNumber(456);
+
+        mockNode = mock(NetconfNode.class);
+        mockSession = mock(NetconfClientSession.class);
+
+        instance1 = new ContextKey(address1, port1);
+        instance2 = new ContextKey(address2, port2);
+        instance3 = new ContextKey(address1, port2);
+        instance4 = new ContextKey(address2, port1);
+
+        Host mockHost = mock(Host.class);
+        when(mockHost.getIpAddress()).thenReturn(address1);
+        when(mockNode.getHost()).thenReturn(mockHost);
+
+        when(mockNode.getPort()).thenReturn(port1);
+    }
+
+    @Test
+    public void hashCodesForDifferentKeysAreDifferent() {
+        // expect
+        assertNotEquals(instance1.hashCode(), instance2.hashCode());
+        assertNotEquals(instance1.hashCode(), 0);
+        assertNotEquals(instance2.hashCode(), 0);
+    }
+
+    @Test
+    public void variousFlavorsOfEqualWork() {
+        // expect
+        assertTrue(instance1.equals(instance1));
+        assertFalse(instance1.equals(null));
+        assertFalse(instance1.equals(new Long(123456)));
+        assertFalse(instance1.equals(instance2));
+        assertFalse(instance1.equals(instance3));
+        assertFalse(instance1.equals(instance4));
+    }
+
+    @Test
+    public void newContextCanBeCreatedFromASocketAddress() throws UnknownHostException {
+        // given
+        Inet4Address someAddressIpv4 = (Inet4Address) InetAddress.getByName("1.2.3.4");
+        Inet6Address someAddressIpv6 = (Inet6Address) InetAddress.getByName("::1");
+        // and
+        ContextKey key1 = ContextKey.from(new InetSocketAddress(someAddressIpv4, 123));
+        ContextKey key2 = ContextKey.from(new InetSocketAddress(someAddressIpv6, 123));
+        // expect
+        assertNotNull(key1);
+        assertNotNull(key1.toString());
+        assertNotNull(key2);
+        assertNotNull(key2.toString());
+    }
+
+    @Test
+    public void newContextCanBeCreatedFromANetconfNode() {
+        // expect
+        assertNotNull(ContextKey.from(mockNode));
+    }
+}
index 17db1dafb2ea0e974e1e4dee67aa7b01533409a4..196f843cb48045311717a3246e79c21f4756295a 100644 (file)
                 <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>
index f6783c6219a7c659764c127f6b95b18307727a0c..1f80ee8e561c50d2b09efda1c653c4d671ec70d4 100644 (file)
     <module>tools</module>
     <module>netconf-console</module>
 
+    <module>callhome-model</module>
+    <module>callhome-protocol</module>
+    <module>callhome-provider</module>
+
     <module>netconf-artifacts</module>
   </modules>