BUG-1541 Netconf device simulating testtool 87/9887/3
authorMaros Marsalek <mmarsale@cisco.com>
Tue, 12 Aug 2014 13:31:27 +0000 (15:31 +0200)
committerMaros Marsalek <mmarsale@cisco.com>
Mon, 18 Aug 2014 09:11:22 +0000 (11:11 +0200)
Produces executable jar file.

The jar can simulate arbitrary number of netconf devices that listen on configured ports
Schemas are loaded from a provided folder
Can also generate ODL initial config files for started simulated devices to support testing with ODL distribution

Change-Id: I8fce73fa7c568a272c29073f26a4e8aafeebfd82
Signed-off-by: Maros Marsalek <mmarsale@cisco.com>
opendaylight/netconf/netconf-testtool/pom.xml [new file with mode: 0644]
opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/AcceptingAuthProvider.java [new file with mode: 0644]
opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/Main.java [new file with mode: 0644]
opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/ModuleBuilderCapability.java [new file with mode: 0644]
opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/NetconfDeviceSimulator.java [new file with mode: 0644]
opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/SimulatedGet.java [new file with mode: 0644]
opendaylight/netconf/pom.xml

diff --git a/opendaylight/netconf/netconf-testtool/pom.xml b/opendaylight/netconf/netconf-testtool/pom.xml
new file mode 100644 (file)
index 0000000..ae0bb76
--- /dev/null
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2014 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.controller</groupId>
+        <artifactId>netconf-subsystem</artifactId>
+        <version>0.2.5-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>netconf-testtool</artifactId>
+    <name>${project.artifactId}</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>net.sourceforge.argparse4j</groupId>
+            <artifactId>argparse4j</artifactId>
+            <version>0.4.3</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netconf-netty-util</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.controller</groupId>
+            <artifactId>commons.logback_settings</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.controller</groupId>
+            <artifactId>config-netconf-connector</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.controller</groupId>
+            <artifactId>netconf-connector-config</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.controller</groupId>
+            <artifactId>logback-config</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.yangtools</groupId>
+            <artifactId>mockito-configuration</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>xmlunit</groupId>
+            <artifactId>xmlunit</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>config-util</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netconf-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netconf-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netconf-impl</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netconf-mapping-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netconf-monitoring</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netconf-ssh</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netty-config-api</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <configuration></configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <phase>package</phase>
+                        <configuration>
+                            <!-- TODO investigate why jar fails without this filter-->
+                            <filters>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/*.SF</exclude>
+                                        <exclude>META-INF/*.DSA</exclude>
+                                        <exclude>META-INF/*.RSA</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                            <transformers>
+                                <transformer
+                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    <mainClass>org.opendaylight.controller.netconf.test.tool.Main</mainClass>
+                                </transformer>
+                            </transformers>
+                            <shadedArtifactAttached>true</shadedArtifactAttached>
+                            <shadedClassifierName>executable</shadedClassifierName>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/AcceptingAuthProvider.java b/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/AcceptingAuthProvider.java
new file mode 100644 (file)
index 0000000..35f2345
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2014 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.test.tool;
+
+import java.io.File;
+import java.io.IOException;
+import org.opendaylight.controller.netconf.ssh.authentication.AuthProvider;
+import org.opendaylight.controller.netconf.ssh.authentication.PEMGenerator;
+
+class AcceptingAuthProvider implements AuthProvider {
+    private final String privateKeyPEMString;
+
+    public AcceptingAuthProvider() {
+        try {
+            this.privateKeyPEMString = PEMGenerator.readOrGeneratePK(new File("PK"));
+        } catch (final IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public synchronized boolean authenticated(final String username, final String password) {
+        return true;
+    }
+
+    @Override
+    public char[] getPEMAsCharArray() {
+        return privateKeyPEMString.toCharArray();
+    }
+}
diff --git a/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/Main.java b/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/Main.java
new file mode 100644 (file)
index 0000000..59e9f4c
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2014 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.test.tool;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+
+import net.sourceforge.argparse4j.ArgumentParsers;
+import net.sourceforge.argparse4j.annotation.Arg;
+import net.sourceforge.argparse4j.inf.ArgumentParser;
+import net.sourceforge.argparse4j.inf.ArgumentParserException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+
+public final class Main {
+
+    // TODO add logback config
+
+    // TODO make exi configurable
+
+    private static final Logger LOG = LoggerFactory.getLogger(Main.class);
+
+    static class Params {
+
+        @Arg(dest = "schemas-dir")
+        public File schemasDir;
+
+        @Arg(dest = "devices-count")
+        public int deviceCount;
+
+        @Arg(dest = "starting-port")
+        public int startingPort;
+
+        @Arg(dest = "generate-configs-dir")
+        public File generateConfigsDir;
+
+        @Arg(dest = "generate-configs-batch-size")
+        public int generateConfigBatchSize;
+
+        @Arg(dest = "ssh")
+        public boolean ssh;
+
+        static ArgumentParser getParser() {
+            final ArgumentParser parser = ArgumentParsers.newArgumentParser("netconf testool");
+            parser.addArgument("--devices-count")
+                    .type(Integer.class)
+                    .setDefault(1)
+                    .type(Integer.class)
+                    .help("Number of simulated netconf devices to spin")
+                    .dest("devices-count");
+
+            parser.addArgument("--schemas-dir")
+                    .type(File.class)
+                    .required(true)
+                    .help("Directory containing yang schemas to describe simulated devices")
+                    .dest("schemas-dir");
+
+            parser.addArgument("--starting-port")
+                    .type(Integer.class)
+                    .setDefault(17830)
+                    .help("First port for simulated device. Each other device will have previous+1 port number")
+                    .dest("starting-port");
+
+            parser.addArgument("--generate-configs-batch-size")
+                    .type(Integer.class)
+                    .setDefault(100)
+                    .help("Number of connector configs per generated file")
+                    .dest("generate-configs-batch-size");
+
+            parser.addArgument("--generate-configs-dir")
+                    .type(File.class)
+                    .help("Directory where initial config files for ODL distribution should be generated")
+                    .dest("generate-configs-dir");
+
+            parser.addArgument("--ssh")
+                    .type(Boolean.class)
+                    .setDefault(true)
+                    .help("Whether to use ssh for transport or just pure tcp")
+                    .dest("ssh");
+
+            return parser;
+        }
+
+        void validate() {
+            checkArgument(deviceCount > 0, "Device count has to be > 0");
+            checkArgument(startingPort > 1024, "Starting port has to be > 1024");
+
+            checkArgument(schemasDir.exists(), "Schemas dir has to exist");
+            checkArgument(schemasDir.isDirectory(), "Schemas dir has to be a directory");
+            checkArgument(schemasDir.canRead(), "Schemas dir has to be readable");
+        }
+    }
+
+    public static void main(final String[] args) {
+        final Params params = parseArgs(args, Params.getParser());
+        params.validate();
+
+        final NetconfDeviceSimulator netconfDeviceSimulator = new NetconfDeviceSimulator();
+        try {
+            final List<Integer> openDevices = netconfDeviceSimulator.start(params);
+            if(params.generateConfigsDir != null) {
+                new ConfigGenerator(params.generateConfigsDir, openDevices).generate(params.ssh, params.generateConfigBatchSize);
+            }
+        } catch (final Exception e) {
+            LOG.error("Unhandled exception", e);
+            netconfDeviceSimulator.close();
+            System.exit(1);
+        }
+
+        // Block main thread
+        synchronized (netconfDeviceSimulator) {
+            try {
+                netconfDeviceSimulator.wait();
+            } catch (final InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+
+    private static Params parseArgs(final String[] args, final ArgumentParser parser) {
+        final Params opt = new Params();
+        try {
+            parser.parseArgs(args, opt);
+            return opt;
+        } catch (final ArgumentParserException e) {
+            parser.handleError(e);
+        }
+
+        System.exit(1);
+        return null;
+    }
+
+    private static class ConfigGenerator {
+        public static final String NETCONF_CONNECTOR_XML = "/initial/99-netconf-connector.xml";
+        public static final String NETCONF_CONNECTOR_NAME = "controller-config";
+        public static final String NETCONF_CONNECTOR_PORT = "1830";
+        public static final String NETCONF_USE_SSH = "false";
+        public static final String SIM_DEVICE_SUFFIX = "-sim-device";
+
+        private final File directory;
+        private final List<Integer> openDevices;
+
+        public ConfigGenerator(final File directory, final List<Integer> openDevices) {
+            this.directory = directory;
+            this.openDevices = openDevices;
+        }
+
+        public void generate(final boolean useSsh, final int batchSize) {
+            if(directory.exists() == false) {
+                checkState(directory.mkdirs(), "Unable to create folder %s" + directory);
+            }
+
+            try(InputStream stream = Main.class.getResourceAsStream(NETCONF_CONNECTOR_XML)) {
+                checkNotNull(stream, "Cannot load %s", NETCONF_CONNECTOR_XML);
+                String configBlueprint = CharStreams.toString(new InputStreamReader(stream, Charsets.UTF_8));
+
+                // TODO make address configurable
+                checkState(configBlueprint.contains(NETCONF_CONNECTOR_NAME));
+                checkState(configBlueprint.contains(NETCONF_CONNECTOR_PORT));
+                checkState(configBlueprint.contains(NETCONF_USE_SSH));
+                configBlueprint = configBlueprint.replace(NETCONF_CONNECTOR_NAME, "%s");
+                configBlueprint = configBlueprint.replace(NETCONF_CONNECTOR_PORT, "%s");
+                configBlueprint = configBlueprint.replace(NETCONF_USE_SSH, "%s");
+
+                final String before = configBlueprint.substring(0, configBlueprint.indexOf("<module>"));
+                final String middleBlueprint = configBlueprint.substring(configBlueprint.indexOf("<module>"), configBlueprint.indexOf("</module>") + "</module>".length());
+                final String after = configBlueprint.substring(configBlueprint.indexOf("</module>") + "</module>".length());
+
+                int connectorCount = 0;
+                Integer batchStart = null;
+                StringBuilder b = new StringBuilder();
+                b.append(before);
+
+                for (final Integer openDevice : openDevices) {
+                    if(batchStart == null) {
+                        batchStart = openDevice;
+                    }
+
+                    final String name = String.valueOf(openDevice) + SIM_DEVICE_SUFFIX;
+                    final String configContent = String.format(middleBlueprint, name, String.valueOf(openDevice), String.valueOf(!useSsh));
+                    b.append(configContent);
+                    connectorCount++;
+                    if(connectorCount == batchSize) {
+                        b.append(after);
+                        Files.write(b.toString(), new File(directory, String.format("simulated-devices_%d-%d.xml", batchStart, openDevice)), Charsets.UTF_8);
+                        connectorCount = 0;
+                        b = new StringBuilder();
+                        b.append(before);
+                        batchStart = null;
+                    }
+                }
+
+                // Write remaining
+                if(connectorCount != 0) {
+                    b.append(after);
+                    Files.write(b.toString(), new File(directory, String.format("simulated-devices_%d-%d.xml", batchStart, openDevices.get(openDevices.size() - 1))), Charsets.UTF_8);
+                }
+
+                LOG.info("Config files generated in {}", directory);
+            } catch (final IOException e) {
+                throw new RuntimeException("Unable to generate config files", e);
+            }
+        }
+    }
+}
diff --git a/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/ModuleBuilderCapability.java b/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/ModuleBuilderCapability.java
new file mode 100644 (file)
index 0000000..1a68f55
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2014 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.test.tool;
+
+import com.google.common.base.Optional;
+import java.util.Date;
+import java.util.List;
+import org.opendaylight.controller.netconf.confignetconfconnector.util.Util;
+import org.opendaylight.controller.netconf.mapping.api.Capability;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.parser.builder.impl.ModuleBuilder;
+
+final class ModuleBuilderCapability implements Capability {
+    private static final Date NO_REVISION = new Date(0);
+    private final ModuleBuilder input;
+    private final Optional<String> content;
+
+    public ModuleBuilderCapability(final ModuleBuilder input, final String inputStream) {
+        this.input = input;
+        this.content = Optional.of(inputStream);
+    }
+
+    @Override
+    public String getCapabilityUri() {
+        // FIXME capabilities in Netconf-impl need to check for NO REVISION
+        final String withoutRevision = getModuleNamespace().get() + "?module=" + getModuleName().get();
+        return hasRevision() ? withoutRevision + "&revision=" + Util.writeDate(input.getRevision()) : withoutRevision;
+    }
+
+    @Override
+    public Optional<String> getModuleNamespace() {
+        return Optional.of(input.getNamespace().toString());
+    }
+
+    @Override
+    public Optional<String> getModuleName() {
+        return Optional.of(input.getName());
+    }
+
+    @Override
+    public Optional<String> getRevision() {
+        return Optional.of(hasRevision() ? QName.formattedRevision(input.getRevision()) : "");
+    }
+
+    private boolean hasRevision() {
+        return !input.getRevision().equals(NO_REVISION);
+    }
+
+    @Override
+    public Optional<String> getCapabilitySchema() {
+        return content;
+    }
+
+    @Override
+    public Optional<List<String>> getLocation() {
+        return Optional.absent();
+    }
+}
diff --git a/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/NetconfDeviceSimulator.java b/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/NetconfDeviceSimulator.java
new file mode 100644 (file)
index 0000000..b21c02a
--- /dev/null
@@ -0,0 +1,356 @@
+/*
+ * Copyright (c) 2014 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.test.tool;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.io.CharStreams;
+import com.google.common.util.concurrent.CheckedFuture;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.local.LocalAddress;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.util.HashedWheelTimer;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.management.ManagementFactory;
+import java.net.Inet4Address;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.AbstractMap;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutionException;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.tree.ParseTreeWalker;
+import org.opendaylight.controller.netconf.api.monitoring.NetconfManagementSession;
+import org.opendaylight.controller.netconf.impl.DefaultCommitNotificationProducer;
+import org.opendaylight.controller.netconf.impl.NetconfServerDispatcher;
+import org.opendaylight.controller.netconf.impl.NetconfServerSessionNegotiatorFactory;
+import org.opendaylight.controller.netconf.impl.SessionIdProvider;
+import org.opendaylight.controller.netconf.impl.osgi.NetconfMonitoringServiceImpl;
+import org.opendaylight.controller.netconf.impl.osgi.SessionMonitoringService;
+import org.opendaylight.controller.netconf.mapping.api.Capability;
+import org.opendaylight.controller.netconf.mapping.api.NetconfOperation;
+import org.opendaylight.controller.netconf.mapping.api.NetconfOperationProvider;
+import org.opendaylight.controller.netconf.mapping.api.NetconfOperationService;
+import org.opendaylight.controller.netconf.mapping.api.NetconfOperationServiceSnapshot;
+import org.opendaylight.controller.netconf.monitoring.osgi.NetconfMonitoringOperationService;
+import org.opendaylight.controller.netconf.ssh.NetconfSSHServer;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceException;
+import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
+import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
+import org.opendaylight.yangtools.yang.model.repo.spi.PotentialSchemaSource;
+import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceListener;
+import org.opendaylight.yangtools.yang.model.repo.util.FilesystemSchemaSourceCache;
+import org.opendaylight.yangtools.yang.parser.builder.impl.BuilderUtils;
+import org.opendaylight.yangtools.yang.parser.builder.impl.ModuleBuilder;
+import org.opendaylight.yangtools.yang.parser.impl.YangParserListenerImpl;
+import org.opendaylight.yangtools.yang.parser.repo.SharedSchemaRepository;
+import org.opendaylight.yangtools.yang.parser.util.ASTSchemaSource;
+import org.opendaylight.yangtools.yang.parser.util.TextToASTTransformer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NetconfDeviceSimulator implements Closeable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NetconfDeviceSimulator.class);
+
+    public static final int CONNECTION_TIMEOUT_MILLIS = 20000;
+
+    private final NioEventLoopGroup nettyThreadgroup;
+    private final HashedWheelTimer hashedWheelTimer;
+    private final List<Channel> devicesChannels = Lists.newArrayList();
+
+    public NetconfDeviceSimulator() {
+        this(new NioEventLoopGroup(), new HashedWheelTimer());
+    }
+
+    public NetconfDeviceSimulator(final NioEventLoopGroup eventExecutors, final HashedWheelTimer hashedWheelTimer) {
+        this.nettyThreadgroup = eventExecutors;
+        this.hashedWheelTimer = hashedWheelTimer;
+    }
+
+    private NetconfServerDispatcher createDispatcher(final Map<ModuleBuilder, String> moduleBuilders) {
+
+        final Set<Capability> capabilities = Sets.newHashSet(Collections2.transform(moduleBuilders.keySet(), new Function<ModuleBuilder, Capability>() {
+            @Override
+            public Capability apply(final ModuleBuilder input) {
+                return new ModuleBuilderCapability(input, moduleBuilders.get(input));
+            }
+        }));
+
+        final SessionIdProvider idProvider = new SessionIdProvider();
+
+        final SimulatedOperationProvider simulatedOperationProvider = new SimulatedOperationProvider(idProvider, capabilities);
+        final NetconfMonitoringOperationService monitoringService = new NetconfMonitoringOperationService(new NetconfMonitoringServiceImpl(simulatedOperationProvider));
+        simulatedOperationProvider.addService(monitoringService);
+
+        final DefaultCommitNotificationProducer commitNotifier = new DefaultCommitNotificationProducer(ManagementFactory.getPlatformMBeanServer());
+
+        final NetconfServerSessionNegotiatorFactory serverNegotiatorFactory = new NetconfServerSessionNegotiatorFactory(
+                hashedWheelTimer, simulatedOperationProvider, idProvider, CONNECTION_TIMEOUT_MILLIS, commitNotifier, new LoggingMonitoringService());
+
+        final NetconfServerDispatcher.ServerChannelInitializer serverChannelInitializer = new NetconfServerDispatcher.ServerChannelInitializer(
+                serverNegotiatorFactory);
+        return new NetconfServerDispatcher(serverChannelInitializer, nettyThreadgroup, nettyThreadgroup);
+    }
+
+    private Map<ModuleBuilder, String> toModuleBuilders(final Map<SourceIdentifier, Map.Entry<ASTSchemaSource, YangTextSchemaSource>> sources) {
+            final Map<SourceIdentifier, ParserRuleContext> asts = Maps.transformValues(sources,  new Function<Map.Entry<ASTSchemaSource, YangTextSchemaSource>, ParserRuleContext>() {
+                @Override
+                public ParserRuleContext apply(final Map.Entry<ASTSchemaSource, YangTextSchemaSource> input) {
+                    return input.getKey().getAST();
+                }
+            });
+            final Map<String, TreeMap<Date, URI>> namespaceContext = BuilderUtils.createYangNamespaceContext(
+                    asts.values(), Optional.<SchemaContext>absent());
+
+            final ParseTreeWalker walker = new ParseTreeWalker();
+            final Map<ModuleBuilder, String> sourceToBuilder = new HashMap<>();
+
+            for (final Map.Entry<SourceIdentifier, ParserRuleContext> entry : asts.entrySet()) {
+                final ModuleBuilder moduleBuilder = YangParserListenerImpl.create(namespaceContext, entry.getKey().getName(),
+                        walker, entry.getValue()).getModuleBuilder();
+
+                try(InputStreamReader stream = new InputStreamReader(sources.get(entry.getKey()).getValue().openStream(), Charsets.UTF_8)) {
+                    sourceToBuilder.put(moduleBuilder, CharStreams.toString(stream));
+                } catch (final IOException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+
+            return sourceToBuilder;
+        }
+
+
+    public List<Integer> start(final Main.Params params) {
+        final Map<ModuleBuilder, String> moduleBuilders = parseSchemasToModuleBuilders(params);
+
+        final NetconfServerDispatcher dispatcher = createDispatcher(moduleBuilders);
+
+        int currentPort = params.startingPort;
+
+        final List<Integer> openDevices = Lists.newArrayList();
+        for (int i = 0; i < params.deviceCount; i++) {
+            final InetSocketAddress address = getAddress(currentPort);
+
+            final ChannelFuture server;
+            if(params.ssh) {
+                final LocalAddress tcpLocalAddress = new LocalAddress(address.toString());
+
+                server = dispatcher.createLocalServer(tcpLocalAddress);
+                try {
+                    NetconfSSHServer.start(currentPort, tcpLocalAddress, new AcceptingAuthProvider(), nettyThreadgroup);
+                } catch (final Exception e) {
+                    LOG.warn("Cannot start simulated device on {}, skipping", address, e);
+                    // Close local server and continue
+                    server.cancel(true);
+                    if(server.isDone()) {
+                        server.channel().close();
+                    }
+                    continue;
+                } finally {
+                    currentPort++;
+                }
+
+                try {
+                    server.get();
+                } catch (final InterruptedException e) {
+                    throw new RuntimeException(e);
+                } catch (final ExecutionException e) {
+                    LOG.warn("Cannot start ssh simulated device on {}, skipping", address, e);
+                    continue;
+                }
+
+                LOG.debug("Simulated SSH device started on {}", address);
+
+            } else {
+                server = dispatcher.createServer(address);
+                currentPort++;
+
+                try {
+                    server.get();
+                } catch (final InterruptedException e) {
+                    throw new RuntimeException(e);
+                } catch (final ExecutionException e) {
+                    LOG.warn("Cannot start tcp simulated device on {}, skipping", address, e);
+                    continue;
+                }
+
+                LOG.debug("Simulated TCP device started on {}", address);
+            }
+
+            devicesChannels.add(server.channel());
+            openDevices.add(currentPort - 1);
+
+        }
+
+        if(openDevices.size() == params.deviceCount) {
+            LOG.info("All simulated devices started successfully from port {} to {}", params.startingPort, currentPort);
+        } else {
+            LOG.warn("Not all simulated devices started successfully. Started devices ar on ports {}", openDevices);
+        }
+
+        return openDevices;
+    }
+
+    private Map<ModuleBuilder, String> parseSchemasToModuleBuilders(final Main.Params params) {
+        final SharedSchemaRepository consumer = new SharedSchemaRepository("netconf-simulator");
+        consumer.registerSchemaSourceListener(TextToASTTransformer.create(consumer, consumer));
+
+        final Set<SourceIdentifier> loadedSources = Sets.newHashSet();
+
+        consumer.registerSchemaSourceListener(new SchemaSourceListener() {
+            @Override
+            public void schemaSourceEncountered(final SchemaSourceRepresentation schemaSourceRepresentation) {}
+
+            @Override
+            public void schemaSourceRegistered(final Iterable<PotentialSchemaSource<?>> potentialSchemaSources) {
+                for (final PotentialSchemaSource<?> potentialSchemaSource : potentialSchemaSources) {
+                    loadedSources.add(potentialSchemaSource.getSourceIdentifier());
+                }
+            }
+
+            @Override
+            public void schemaSourceUnregistered(final PotentialSchemaSource<?> potentialSchemaSource) {}
+        });
+
+        final FilesystemSchemaSourceCache<YangTextSchemaSource> cache = new FilesystemSchemaSourceCache<>(consumer, YangTextSchemaSource.class, params.schemasDir);
+        consumer.registerSchemaSourceListener(cache);
+
+        final Map<SourceIdentifier, Map.Entry<ASTSchemaSource, YangTextSchemaSource>> asts = Maps.newHashMap();
+        for (final SourceIdentifier loadedSource : loadedSources) {
+                try {
+                    final CheckedFuture<ASTSchemaSource, SchemaSourceException> ast = consumer.getSchemaSource(loadedSource, ASTSchemaSource.class);
+                    final CheckedFuture<YangTextSchemaSource, SchemaSourceException> text = consumer.getSchemaSource(loadedSource, YangTextSchemaSource.class);
+                    asts.put(loadedSource, new AbstractMap.SimpleEntry<>(ast.get(), text.get()));
+                } catch (final InterruptedException e) {
+                    throw new RuntimeException(e);
+                } catch (final ExecutionException e) {
+                    throw new RuntimeException("Cannot parse schema context", e);
+                }
+        }
+        return toModuleBuilders(asts);
+    }
+
+    private static InetSocketAddress getAddress(final int port) {
+        try {
+            // TODO make address configurable
+            return new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), port);
+        } catch (final UnknownHostException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void close() {
+        for (final Channel deviceCh : devicesChannels) {
+            deviceCh.close();
+        }
+        nettyThreadgroup.shutdownGracefully();
+        // close Everything
+    }
+
+    private static class SimulatedOperationProvider implements NetconfOperationProvider {
+        private final SessionIdProvider idProvider;
+        private final Set<NetconfOperationService> netconfOperationServices;
+
+
+        public SimulatedOperationProvider(final SessionIdProvider idProvider, final Set<Capability> caps) {
+            this.idProvider = idProvider;
+            final SimulatedOperationService simulatedOperationService = new SimulatedOperationService(caps, idProvider.getCurrentSessionId());
+            this.netconfOperationServices = Sets.<NetconfOperationService>newHashSet(simulatedOperationService);
+        }
+
+        @Override
+        public NetconfOperationServiceSnapshot openSnapshot(final String sessionIdForReporting) {
+            return new SimulatedServiceSnapshot(idProvider, netconfOperationServices);
+        }
+
+        public void addService(final NetconfOperationService monitoringService) {
+            netconfOperationServices.add(monitoringService);
+        }
+
+        private static class SimulatedServiceSnapshot implements NetconfOperationServiceSnapshot {
+            private final SessionIdProvider idProvider;
+            private final Set<NetconfOperationService> netconfOperationServices;
+
+            public SimulatedServiceSnapshot(final SessionIdProvider idProvider, final Set<NetconfOperationService> netconfOperationServices) {
+                this.idProvider = idProvider;
+                this.netconfOperationServices = netconfOperationServices;
+            }
+
+            @Override
+            public String getNetconfSessionIdForReporting() {
+                return String.valueOf(idProvider.getCurrentSessionId());
+            }
+
+            @Override
+            public Set<NetconfOperationService> getServices() {
+                return netconfOperationServices;
+            }
+
+            @Override
+            public void close() throws Exception {}
+        }
+
+        static class SimulatedOperationService implements NetconfOperationService {
+            private final Set<Capability> capabilities;
+            private static SimulatedGet sGet;
+
+            public SimulatedOperationService(final Set<Capability> capabilities, final long currentSessionId) {
+                this.capabilities = capabilities;
+                sGet = new SimulatedGet(String.valueOf(currentSessionId));
+            }
+
+            @Override
+            public Set<Capability> getCapabilities() {
+                return capabilities;
+            }
+
+            @Override
+            public Set<NetconfOperation> getNetconfOperations() {
+                return Sets.<NetconfOperation>newHashSet(sGet);
+            }
+
+            @Override
+            public void close() {
+            }
+
+        }
+    }
+
+    private class LoggingMonitoringService implements SessionMonitoringService {
+        @Override
+        public void onSessionUp(final NetconfManagementSession session) {
+            LOG.debug("Session {} established", session);
+        }
+
+        @Override
+        public void onSessionDown(final NetconfManagementSession session) {
+            LOG.debug("Session {} down", session);
+        }
+    }
+
+}
diff --git a/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/SimulatedGet.java b/opendaylight/netconf/netconf-testtool/src/main/java/org/opendaylight/controller/netconf/test/tool/SimulatedGet.java
new file mode 100644 (file)
index 0000000..b1938c8
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2014 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.test.tool;
+
+import com.google.common.base.Optional;
+import org.opendaylight.controller.netconf.api.NetconfDocumentedException;
+import org.opendaylight.controller.netconf.api.xml.XmlNetconfConstants;
+import org.opendaylight.controller.netconf.confignetconfconnector.operations.AbstractConfigNetconfOperation;
+import org.opendaylight.controller.netconf.util.xml.XmlElement;
+import org.opendaylight.controller.netconf.util.xml.XmlUtil;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+class SimulatedGet extends AbstractConfigNetconfOperation {
+
+    SimulatedGet(final String netconfSessionIdForReporting) {
+        super(null, netconfSessionIdForReporting);
+    }
+
+    @Override
+    protected Element handleWithNoSubsequentOperations(final Document document, final XmlElement operationElement) throws NetconfDocumentedException {
+        return XmlUtil.createElement(document, XmlNetconfConstants.DATA_KEY, Optional.<String>absent());
+    }
+
+    @Override
+    protected String getOperationName() {
+        return XmlNetconfConstants.GET;
+    }
+}
index e55ec69..b1b410a 100644 (file)
         <module>netconf-it</module>
       </modules>
     </profile>
+
+    <profile>
+        <id>testtool</id>
+        <activation>
+            <activeByDefault>false</activeByDefault>
+        </activation>
+        <modules>
+            <module>netconf-testtool</module>
+      </modules>
+    </profile>
   </profiles>
 </project>