<artifactId>netconf-mapping-api</artifactId>
<version>${netconf.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.controller</groupId>
+ <artifactId>netconf-ssh</artifactId>
+ <version>${netconf.version}</version>
+ </dependency>
<dependency>
<groupId>org.opendaylight.controller</groupId>
<artifactId>config-netconf-connector</artifactId>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-model-api</artifactId>
</dependency>
-
<dependency>
<groupId>org.opendaylight.yangtools.model</groupId>
<artifactId>yang-ext</artifactId>
</dependency>
-
<dependency>
<groupId>org.opendaylight.controller.thirdparty</groupId>
<artifactId>ganymed</artifactId>
netconf.tcp.address=0.0.0.0
netconf.tcp.port=8383
-#netconf.tls.address=127.0.0.1
-#netconf.tls.port=8384
-#netconf.tls.keystore=
-#netconf.tls.keystore.password=
+
+netconf.ssh.address=0.0.0.0
+netconf.ssh.port=1830
netconf.config.persister.storageAdapterClass=org.opendaylight.controller.config.persist.storage.file.FileStorageAdapter
fileStorage=configuration/controller.config
private final long sessionId;
private boolean up = false;
private static final Logger logger = LoggerFactory.getLogger(NetconfSession.class);
- private static final int T = 0;
protected NetconfSession(SessionListener sessionListener, Channel channel, long sessionId) {
this.sessionListener = sessionListener;
public Collection<String> getServerCapabilities() {
return capabilities;
}
-
- public Channel getChannel(){
- return channel;
- }
-
}
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
+import static java.util.Collections.emptyList;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
-import static org.mockito.internal.util.Checks.checkNotNull;
public class NetconfITTest extends AbstractConfigTest {
//
private static final InetSocketAddress tcpAddress = new InetSocketAddress("127.0.0.1", 12023);
- private static final InetSocketAddress sshAddress = new InetSocketAddress("127.0.0.1", 830);
+ private static final InetSocketAddress sshAddress = new InetSocketAddress("127.0.0.1", 10830);
private static final String USERNAME = "netconf";
private static final String PASSWORD = "netconf";
"/META-INF/yang/config-test.yang", "/META-INF/yang/config-test-impl.yang",
"/META-INF/yang/ietf-inet-types.yang");
final Collection<InputStream> yangDependencies = new ArrayList<>();
+ List<String> failedToFind = new ArrayList<>();
for (String path : paths) {
- final InputStream is = checkNotNull(NetconfITTest.class.getResourceAsStream(path), path + " not found");
- yangDependencies.add(is);
+ InputStream resourceAsStream = NetconfITTest.class.getResourceAsStream(path);
+ if (resourceAsStream == null) {
+ failedToFind.add(path);
+ } else {
+ yangDependencies.add(resourceAsStream);
+ }
}
+ assertEquals("Some yang files were not found",emptyList(), failedToFind);
return yangDependencies;
}
return netconfClient;
}
- private class TestSSHServer implements Runnable {
- public void run() {
- try {
- NetconfSSHServer.start();
- } catch (Exception e) {
- logger.info(e.getMessage());
- }
- }
- }
private void startSSHServer() throws Exception{
logger.info("Creating SSH server");
- Thread sshServerThread = new Thread(new TestSSHServer());
+ Thread sshServerThread = new Thread(NetconfSSHServer.start(10830,tcpAddress));
sshServerThread.setDaemon(true);
sshServerThread.start();
logger.info("SSH server on");
@Test
public void sshTest() throws Exception {
startSSHServer();
+ logger.info("creating connection");
Connection conn = new Connection(sshAddress.getHostName(),sshAddress.getPort());
Assert.assertNotNull(conn);
- try {
- conn.connect();
- boolean isAuthenticated = conn.authenticateWithPassword(USERNAME,PASSWORD);
- assertTrue(isAuthenticated);
- Session sess = conn.openSession();
- sess.startSubSystem("netconf");
-// sess.requestPTY("");
- } catch (IOException e) {
- e.printStackTrace();
- }
+ logger.info("connection created");
+ conn.connect();
+ boolean isAuthenticated = conn.authenticateWithPassword(USERNAME,PASSWORD);
+ assertTrue(isAuthenticated);
+ logger.info("user authenticated");
+ final Session sess = conn.openSession();
+ sess.startSubSystem("netconf");
+ logger.info("user authenticated");
+ sess.getStdin().write(XmlUtil.toString(this.getConfig.getDocument()).getBytes());
+
+ new Thread(){
+ public void run(){
+ while (true){
+ byte[] bytes = new byte[1024];
+ int c = 0;
+ try {
+ c = sess.getStdout().read(bytes);
+ } catch (IOException e) {
+ e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
+ }
+ logger.info("got data:"+bytes);
+ if (c == 0) break;
+ }
+ }
+ }.join();
}
<groupId>${project.groupId}</groupId>
<artifactId>netconf-util</artifactId>
</dependency>
- <dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>netconf-client</artifactId>
- </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netconf-api</artifactId>
<groupId>org.opendaylight.controller.thirdparty</groupId>
<artifactId>ganymed</artifactId>
</dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
</dependencies>
<build>
io.netty.util,
io.netty.util.concurrent,
javax.annotation,
+ java.net,
javax.net.ssl,
javax.xml.namespace,
javax.xml.parsers,
javax.xml.xpath,
+ org.apache.commons.io,
org.opendaylight.controller.netconf.api,
org.opendaylight.controller.netconf.client,
org.opendaylight.controller.netconf.util,
+ org.opendaylight.controller.netconf.util.osgi,
org.opendaylight.controller.netconf.util.xml,
org.opendaylight.protocol.framework,
org.osgi.framework,
*/
package org.opendaylight.controller.netconf.osgi;
+import com.google.common.base.Optional;
+import java.net.InetSocketAddress;
import org.opendaylight.controller.netconf.ssh.NetconfSSHServer;
+import org.opendaylight.controller.netconf.util.osgi.NetconfConfigUtil;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+/**
+ * Activator for netconf SSH bundle which creates SSH bridge between netconf client and netconf server. Activator
+ * starts SSH Server in its own thread. This thread is closed when activator calls stop() method. Server opens socket
+ * and listen for client connections. Each client connection creation is handled in separate
+ * {@link org.opendaylight.controller.netconf.ssh.threads.SocketThread} thread.
+ * This thread creates two additional threads {@link org.opendaylight.controller.netconf.ssh.threads.IOThread}
+ * forwarding data from/to client.IOThread closes servers session and server connection when it gets -1 on input stream.
+ * {@link org.opendaylight.controller.netconf.ssh.threads.IOThread}'s run method waits for -1 on input stream to finish.
+ * All threads are daemons.
+ **/
public class NetconfSSHActivator implements BundleActivator{
+ private NetconfSSHServer server;
+ private static final Logger logger = LoggerFactory.getLogger(NetconfSSHActivator.class);
+
@Override
public void start(BundleContext context) throws Exception {
- NetconfSSHServer.start();
+
+ logger.trace("Starting netconf SSH bridge.");
+
+ Optional<InetSocketAddress> sshSocketAddressOptional = NetconfConfigUtil.extractSSHNetconfAddress(context);
+ Optional<InetSocketAddress> tcpSocketAddressOptional = NetconfConfigUtil.extractTCPNetconfAddress(context);
+
+ if (sshSocketAddressOptional.isPresent() && tcpSocketAddressOptional.isPresent()){
+ server = NetconfSSHServer.start(sshSocketAddressOptional.get().getPort(),tcpSocketAddressOptional.get());
+ Thread serverThread = new Thread(server,"netconf SSH server thread");
+ serverThread.setDaemon(true);
+ serverThread.start();
+ logger.trace("Netconf SSH bridge up and running.");
+ } else {
+ logger.trace("No valid connection configuration for SSH bridge found.");
+ throw new Exception("No valid connection configuration for SSH bridge found.");
+ }
}
@Override
public void stop(BundleContext context) throws Exception {
-
+ if (server != null){
+ logger.trace("Netconf SSH bridge going down ...");
+ server.stop();
+ logger.trace("Netconf SSH bridge is down ...");
+ }
}
}
-/*\r
- * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved.\r
- *\r
- * This program and the accompanying materials are made available under the\r
- * terms of the Eclipse Public License v1.0 which accompanies this distribution,\r
- * and is available at http://www.eclipse.org/legal/epl-v10.html\r
+/*
+ * Copyright (c) 2013 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
*/
package org.opendaylight.controller.netconf.ssh;
+import java.io.IOException;
+import java.net.InetSocketAddress;
import java.net.ServerSocket;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.annotation.concurrent.ThreadSafe;
+import org.opendaylight.controller.netconf.ssh.threads.SocketThread;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-public class NetconfSSHServer {
+@ThreadSafe
+public class NetconfSSHServer implements Runnable {
private static boolean acceptMore = true;
- private static final int SERVER_PORT = 830;
private ServerSocket ss = null;
+ private static final Logger logger = LoggerFactory.getLogger(NetconfSSHServer.class);
+ private static final AtomicLong sesssionId = new AtomicLong();
+ private final InetSocketAddress clientAddress;
- private NetconfSSHServer() throws Exception{
- this.ss = new ServerSocket(SERVER_PORT);
- while (acceptMore) {
- SocketThread.start(ss.accept());
+ private NetconfSSHServer(int serverPort,InetSocketAddress clientAddress) throws Exception{
+
+ logger.trace("Creating SSH server socket on port {}",serverPort);
+ this.ss = new ServerSocket(serverPort);
+ if (!ss.isBound()){
+ throw new Exception("Socket can't be bound to requested port :"+serverPort);
}
+ logger.trace("Server socket created.");
+ this.clientAddress = clientAddress;
+
}
- public static NetconfSSHServer start() throws Exception {
- return new NetconfSSHServer();
+
+
+ public static NetconfSSHServer start(int serverPort, InetSocketAddress clientAddress) throws Exception {
+ return new NetconfSSHServer(serverPort, clientAddress);
}
public void stop() throws Exception {
- ss.close();
+ acceptMore = false;
+ logger.trace("Closing SSH server socket.");
+ ss.close();
+ logger.trace("SSH server socket closed.");
}
+ @Override
+ public void run() {
+ while (acceptMore) {
+ logger.trace("Starting new socket thread.");
+ try {
+ SocketThread.start(ss.accept(), clientAddress, sesssionId.incrementAndGet());
+ } catch (IOException e) {
+ e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
+ }
+ }
+ }
}
+++ /dev/null
-/*
- * Copyright (c) 2013 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
- */
-package org.opendaylight.controller.netconf.ssh.handler;
-
-import ch.ethz.ssh2.ServerSession;
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.SimpleChannelInboundHandler;
-
-public class SSHChannelInboundHandler extends SimpleChannelInboundHandler {
-
- private ServerSession serverSession;
-
- public SSHChannelInboundHandler(ServerSession serverSession) {
- this.serverSession = serverSession;
- }
-
- @Override
- protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
- this.serverSession.getStdin().write( ((ByteBuf)msg).readBytes(((ByteBuf)msg).readableBytes()).array());
- }
-}
--- /dev/null
+/*
+ * Copyright (c) 2013 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
+ */
+package org.opendaylight.controller.netconf.ssh.threads;
+
+import ch.ethz.ssh2.ServerConnection;
+import ch.ethz.ssh2.ServerSession;
+import java.io.InputStream;
+import java.io.OutputStream;
+import javax.annotation.concurrent.ThreadSafe;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ThreadSafe
+public class IOThread extends Thread {
+
+ private static final Logger logger = LoggerFactory.getLogger(IOThread.class);
+
+ private InputStream inputStream;
+ private OutputStream outputStream;
+ private String id;
+ private ServerSession servSession;
+ private ServerConnection servconnection;
+
+
+ public IOThread (InputStream is, OutputStream os, String id,ServerSession ss, ServerConnection conn){
+ this.inputStream = is;
+ this.outputStream = os;
+ this.servSession = ss;
+ this.servconnection = conn;
+ super.setName(id);
+ logger.trace("IOThread {} created", super.getName());
+ }
+
+ @Override
+ public void run() {
+ logger.trace("thread {} started", super.getName());
+ try {
+ IOUtils.copy(this.inputStream, this.outputStream);
+ } catch (Exception e) {
+ logger.error("inputstream -> outputstream copy error ",e);
+ }
+ logger.trace("closing server session");
+ servSession.close();
+ servconnection.close();
+ logger.trace("thread {} is closing",super.getName());
+ }
+}
-package org.opendaylight.controller.netconf.ssh;
+package org.opendaylight.controller.netconf.ssh.threads;
import ch.ethz.ssh2.AuthenticationResult;
import ch.ethz.ssh2.ServerSession;
import ch.ethz.ssh2.ServerSessionCallback;
import ch.ethz.ssh2.SimpleServerSessionCallback;
-import com.google.common.base.Optional;
-import io.netty.channel.nio.NioEventLoopGroup;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
-import java.nio.ByteBuffer;
-import javax.net.ssl.SSLContext;
-import org.opendaylight.controller.netconf.client.NetconfClient;
-import org.opendaylight.controller.netconf.client.NetconfClientDispatcher;
-import org.opendaylight.controller.netconf.client.NetconfClientSession;
+import javax.annotation.concurrent.ThreadSafe;
import org.opendaylight.controller.netconf.ssh.authentication.RSAKey;
-import org.opendaylight.controller.netconf.ssh.handler.SSHChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
+@ThreadSafe
public class SocketThread implements Runnable, ServerAuthenticationCallback, ServerConnectionCallback
{
private Socket socket;
private static final String USER = "netconf";
private static final String PASSWORD = "netconf";
- private NetconfClient netconfClient;
- private static final InetSocketAddress clientAddress = new InetSocketAddress("127.0.0.1", 12023);
+ private InetSocketAddress clientAddress;
private static final Logger logger = LoggerFactory.getLogger(SocketThread.class);
+ private ServerConnection conn = null;
+ private long sessionId;
- private static ServerConnection conn = null;
-
- public static void start(Socket socket) throws IOException{
- new Thread(new SocketThread(socket)).start();
+ public static void start(Socket socket, InetSocketAddress clientAddress, long sessionId) throws IOException{
+ Thread netconf_ssh_socket_thread = new Thread(new SocketThread(socket,clientAddress,sessionId));
+ netconf_ssh_socket_thread.setDaemon(true);
+ netconf_ssh_socket_thread.start();
}
- private SocketThread(Socket socket) throws IOException {
+ private SocketThread(Socket socket, InetSocketAddress clientAddress, long sessionId) throws IOException {
this.socket = socket;
+ this.clientAddress = clientAddress;
+ this.sessionId = sessionId;
+
+ }
+ @Override
+ public void run() {
conn = new ServerConnection(socket);
RSAKey keyStore = new RSAKey();
conn.setRsaHostKey(keyStore.getPrivateKey());
conn.setAuthenticationCallback(this);
conn.setServerConnectionCallback(this);
- conn.connect();
- }
-
- @Override
- public void run() {
- //noop
+ try {
+ conn.connect();
+ } catch (IOException e) {
+ logger.error("SocketThread error ",e);
+ }
}
public ServerSessionCallback acceptSession(final ServerSession session)
{
SimpleServerSessionCallback cb = new SimpleServerSessionCallback()
{
@Override
- public Runnable requestSubsystem(ServerSession ss, final String subsystem) throws IOException
+ public Runnable requestSubsystem(final ServerSession ss, final String subsystem) throws IOException
{
return new Runnable(){
public void run()
{
if (subsystem.equals("netconf")){
- logger.info("netconf subsystem received");
+ IOThread netconf_ssh_input = null;
+ IOThread netconf_ssh_output = null;
try {
- NetconfClientDispatcher clientDispatcher = null;
- NioEventLoopGroup nioGrup = new NioEventLoopGroup(1);
- clientDispatcher = new NetconfClientDispatcher(Optional.<SSLContext>absent(), nioGrup, nioGrup);
- logger.info("dispatcher created");
- netconfClient = new NetconfClient("ssh_" + clientAddress.toString(),clientAddress,5000,clientDispatcher);
- logger.info("netconf client created");
+ String hostName = clientAddress.getHostName();
+ int portNumber = clientAddress.getPort();
+ final Socket echoSocket = new Socket(hostName, portNumber);
+ logger.trace("echo socket created");
+
+ logger.trace("starting netconf_ssh_input thread");
+ netconf_ssh_input = new IOThread(echoSocket.getInputStream(),ss.getStdin(),"input_thread_"+sessionId,ss,conn);
+ netconf_ssh_input.setDaemon(false);
+ netconf_ssh_input.start();
+
+ logger.trace("starting netconf_ssh_output thread");
+ netconf_ssh_output = new IOThread(ss.getStdout(),echoSocket.getOutputStream(),"output_thread_"+sessionId,ss,conn);
+ netconf_ssh_output.setDaemon(false);
+ netconf_ssh_output.start();
+
} catch (Throwable t){
logger.error(t.getMessage(),t);
+
+ try {
+ if (netconf_ssh_input!=null){
+ netconf_ssh_input.join();
+ }
+ } catch (InterruptedException e) {
+ logger.error("netconf_ssh_input join error ",e);
+ }
+
+ try {
+ if (netconf_ssh_output!=null){
+ netconf_ssh_output.join();
+ }
+ } catch (InterruptedException e) {
+ logger.error("netconf_ssh_output join error ",e);
+ }
+
+ }
+ } else {
+ try {
+ ss.getStdin().write("wrong subsystem requested - closing connection".getBytes());
+ ss.close();
+ } catch (IOException e) {
+ logger.debug("excpetion while sending bad subsystem response",e);
}
}
}
{
public void run()
{
- System.out.println("Client requested " + pty.term + " pty");
+ //noop
}
};
}
{
public void run()
{
- try
- {
- try (NetconfClientSession session = netconfClient.getClientSession())
- {
- session.getChannel().pipeline().addLast(new SSHChannelInboundHandler(ss));
- byte[] bytes = new byte[1024];
- while (true)
- {
- int size = ss.getStdout().read(bytes);
- if (size < 0)
- {
- System.err.println("SESSION EOF");
- return;
- }
- session.getChannel().write(ByteBuffer.wrap(bytes,0,size));
- }
- }
-
- }
- catch (IOException e)
- {
- System.err.println("SESSION DOWN");
- e.printStackTrace();
- }
+ //noop
}
};
}
public String[] getRemainingAuthMethods(ServerConnection sc)
{
- return new String[] { ServerAuthenticationCallback.METHOD_PASSWORD,
- ServerAuthenticationCallback.METHOD_PUBLICKEY };
+ return new String[] { ServerAuthenticationCallback.METHOD_PASSWORD };
}
public AuthenticationResult authenticateWithNone(ServerConnection sc, String username)
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import java.io.IOException;
+import java.net.InetSocketAddress;
import junit.framework.Assert;
-import org.junit.Before;
+import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final String USER = "netconf";
private static final String PASSWORD = "netconf";
private static final String HOST = "127.0.0.1";
- private static final int PORT = 830;
+ private static final int PORT = 1830;
+ private static final InetSocketAddress tcpAddress = new InetSocketAddress("127.0.0.1", 8383);
private static final Logger logger = LoggerFactory.getLogger(SSHServerTest.class);
- private class TestSSHServer implements Runnable {
- public void run() {
- try {
- NetconfSSHServer.start();
- } catch (Exception e) {
- logger.info(e.getMessage());
- }
- }
- }
- @Before
+// @Before
public void startSSHServer() throws Exception{
logger.info("Creating SSH server");
- Thread sshServerThread = new Thread(new TestSSHServer());
+ NetconfSSHServer server = NetconfSSHServer.start(PORT,tcpAddress);
+ Thread sshServerThread = new Thread(server);
sshServerThread.setDaemon(true);
sshServerThread.start();
logger.info("SSH server on");
Session sess = conn.openSession();
logger.info("subsystem netconf");
sess.startSubSystem("netconf");
-// sess.requestPTY("");
+ sess.getStdin().write("<?xml version=\"1.0\" encoding=\"UTF-8\"?><hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"><capabilities><capability>urn:ietf:params:netconf:base:1.1</capability></capabilities></hello>]]>]]>".getBytes());
+ IOUtils.copy(sess.getStdout(), System.out);
} catch (IOException e) {
e.printStackTrace();
}
private static final String PREFIX_PROP = "netconf.";
private enum InfixProp {
- tcp, tls
+ tcp, tls, ssh
}
private static final String PORT_SUFFIX_PROP = ".port";
return extractSomeNetconfAddress(context, InfixProp.tcp);
}
+ public static Optional<InetSocketAddress> extractSSHNetconfAddress(BundleContext context) {
+ return extractSomeNetconfAddress(context, InfixProp.ssh);
+ }
+
+
public static Optional<TLSConfiguration> extractTLSConfiguration(BundleContext context) {
Optional<InetSocketAddress> address = extractSomeNetconfAddress(context, InfixProp.tls);
if (address.isPresent()) {
<extensions>true</extensions>
<configuration>
<instructions>
- <Export-Package>ch.ethz.ssh2</Export-Package>
+ <Export-Package>ch.ethz.ssh2.*</Export-Package>
<Embed-Dependency>ganymed-ssh2;scope=compile</Embed-Dependency>
<Embed-Transitive>true</Embed-Transitive>
</instructions>