From ef26fbbc9928d0842fc5e72d0fa029b12e6cc0f2 Mon Sep 17 00:00:00 2001 From: Ruslan Kashapov Date: Mon, 2 Oct 2023 13:41:53 +0300 Subject: [PATCH] Update netconf device simulator to use new transport Drop use of netconf.northbound.ssh as well as NetconfServerDispatcher, establishing a proper end-to-end integration test of the netconf/trasport integration. JIRA: NETCONF-1106 Change-Id: I3429b28f8a31bfb8af995d25b79b17110823f638 Signed-off-by: Ruslan Kashapov --- .../test/tool/NetconfDeviceSimulator.java | 236 ++++++------- .../netconf/test/tool/LogPropertyCatcher.java | 83 ----- .../netconf/test/tool/TestToolTest.java | 317 +++++++++--------- 3 files changed, 252 insertions(+), 384 deletions(-) delete mode 100644 netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/LogPropertyCatcher.java diff --git a/netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java b/netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java index 969de76fcc..b40938347f 100644 --- a/netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java +++ b/netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java @@ -10,34 +10,23 @@ package org.opendaylight.netconf.test.tool; import static java.util.Objects.requireNonNullElseGet; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ThreadFactoryBuilder; -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.net.BindException; -import java.net.Inet4Address; -import java.net.InetSocketAddress; +import java.net.InetAddress; +import java.net.ServerSocket; import java.net.UnknownHostException; -import java.nio.channels.AsynchronousChannelGroup; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.IntStream; import org.opendaylight.netconf.api.CapabilityURN; -import org.opendaylight.netconf.northbound.ssh.SshProxyServer; -import org.opendaylight.netconf.northbound.ssh.SshProxyServerConfiguration; -import org.opendaylight.netconf.northbound.ssh.SshProxyServerConfigurationBuilder; -import org.opendaylight.netconf.server.NetconfServerDispatcherImpl; +import org.opendaylight.netconf.server.NetconfServerFactoryImpl; import org.opendaylight.netconf.server.NetconfServerSessionNegotiatorFactory; import org.opendaylight.netconf.server.ServerChannelInitializer; import org.opendaylight.netconf.server.api.SessionIdProvider; @@ -48,8 +37,9 @@ import org.opendaylight.netconf.server.api.monitoring.YangModuleCapability; import org.opendaylight.netconf.server.api.operations.NetconfOperationServiceFactory; import org.opendaylight.netconf.server.impl.DefaultSessionIdProvider; import org.opendaylight.netconf.server.osgi.AggregatedNetconfOperationServiceFactory; -import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyPairProvider; -import org.opendaylight.netconf.shaded.sshd.common.util.threads.ThreadUtils; +import org.opendaylight.netconf.shaded.sshd.server.auth.UserAuthFactory; +import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory; +import org.opendaylight.netconf.shaded.sshd.server.auth.pubkey.UserAuthPublicKeyFactory; import org.opendaylight.netconf.test.tool.config.Configuration; import org.opendaylight.netconf.test.tool.customrpc.SettableOperationProvider; import org.opendaylight.netconf.test.tool.monitoring.NetconfMonitoringOperationServiceFactory; @@ -57,7 +47,17 @@ import org.opendaylight.netconf.test.tool.operations.DefaultOperationsCreator; import org.opendaylight.netconf.test.tool.operations.OperationsProvider; import org.opendaylight.netconf.test.tool.rpchandler.SettableOperationRpcProvider; import org.opendaylight.netconf.test.tool.schemacache.SchemaSourceCache; +import org.opendaylight.netconf.transport.api.TransportStack; +import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException; +import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory; +import org.opendaylight.netconf.transport.ssh.ServerFactoryManagerConfigurator; +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.ietf.params.xml.ns.yang.ietf.netconf.server.rev230417.netconf.server.listen.stack.grouping.transport.ssh.ssh.TcpServerParametersBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev230417.TcpServerGrouping; import org.opendaylight.yangtools.yang.common.Revision; +import org.opendaylight.yangtools.yang.common.Uint16; import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.ModuleLike; @@ -77,27 +77,23 @@ import org.slf4j.LoggerFactory; public class NetconfDeviceSimulator implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(NetconfDeviceSimulator.class); - private final NioEventLoopGroup nettyThreadgroup; private final HashedWheelTimer hashedWheelTimer; - private final List devicesChannels = new ArrayList<>(); - private final List sshWrappers = new ArrayList<>(); - private final ScheduledExecutorService minaTimerExecutor; - private final ExecutorService nioExecutor; private final Configuration configuration; + private final List servers; + private final SSHTransportStackFactory sshTransportStackFactory; private EffectiveModelContext schemaContext; private boolean sendFakeSchema = false; public NetconfDeviceSimulator(final Configuration configuration) { this.configuration = configuration; - nettyThreadgroup = new NioEventLoopGroup(); + this.servers = new ArrayList<>(configuration.getDeviceCount()); + this.sshTransportStackFactory = new SSHTransportStackFactory("netconf-device-simulator-threads", + configuration.getThreadPoolSize()); hashedWheelTimer = new HashedWheelTimer(); - minaTimerExecutor = Executors.newScheduledThreadPool(configuration.getThreadPoolSize(), - new ThreadFactoryBuilder().setNameFormat("netconf-ssh-server-mina-timers-%d").build()); - nioExecutor = ThreadUtils.newFixedThreadPool("netconf-ssh-server-nio-group", configuration.getThreadPoolSize()); } - private NetconfServerDispatcherImpl createDispatcher(final Set capabilities, + private ServerChannelInitializer createServerChannelInitializer(final Set capabilities, final SchemaSourceProvider sourceProvider) { final Set transformedCapabilities = new HashSet<>(Collections2.transform(capabilities, input -> { @@ -122,9 +118,7 @@ public class NetconfDeviceSimulator implements Closeable { configuration.getGenerateConfigsTimeout(), monitoringService1, serverCapabilities); - final ServerChannelInitializer serverChannelInitializer = - new ServerChannelInitializer(serverNegotiatorFactory); - return new NetconfServerDispatcherImpl(serverChannelInitializer, nettyThreadgroup, nettyThreadgroup); + return new ServerChannelInitializer(serverNegotiatorFactory); } private NetconfOperationServiceFactory createOperationServiceFactory( @@ -174,114 +168,70 @@ public class NetconfDeviceSimulator implements Closeable { public List start() { final var proto = configuration.isSsh() ? "SSH" : "TCP"; LOG.info("Starting {}, {} simulated devices starting on port {}", - configuration.getDeviceCount(), proto, configuration.getStartingPort()); + configuration.getDeviceCount(), proto, configuration.getStartingPort()); - final SharedSchemaRepository schemaRepo = new SharedSchemaRepository("netconf-simulator"); - final Set capabilities = parseSchemasToModuleCapabilities(schemaRepo); - - final NetconfServerDispatcherImpl dispatcher = createDispatcher(capabilities, + final var schemaRepo = new SharedSchemaRepository("netconf-simulator"); + final var capabilities = parseSchemasToModuleCapabilities(schemaRepo); + final var serverChannelInitializer = createServerChannelInitializer(capabilities, sourceIdentifier -> schemaRepo.getSchemaSource(sourceIdentifier, YangTextSchemaSource.class)); + final var serverFactory = new NetconfServerFactoryImpl(serverChannelInitializer, sshTransportStackFactory); - int currentPort = configuration.getStartingPort(); - - final List openDevices = new ArrayList<>(); + final var ipAddress = getIpAddress(configuration); + final var startingPort = getStartingPort(configuration); + final var deviceCount = configuration.getDeviceCount(); + final var ports = IntStream.range(startingPort, Math.min(startingPort + deviceCount, 65536)) + .mapToObj(Integer::new).toList(); - // Generate key to temp folder - final KeyPairProvider keyPairProvider = new VirtualKeyPairProvider(); + final var openDevices = new ArrayList(ports.size()); + final var configurator = configuration.isSsh() ? createServerFactoryManagerConfigurator(configuration) : null; - final AsynchronousChannelGroup group; - try { - group = AsynchronousChannelGroup.withThreadPool(nioExecutor); - } catch (final IOException e) { - throw new IllegalStateException("Failed to create group", e); - } + LOG.debug("Ports: {}", ports); - for (int i = 0; i < configuration.getDeviceCount(); i++) { - if (currentPort > 65535) { - LOG.warn("Port cannot be greater than 65535, stopping further attempts."); + for (final int port : ports) { + try { + final var connectParams = connectionParams(ipAddress, port); + final var serverFuture = configuration.isSsh() + ? serverFactory.createSshServer(connectParams, null, configurator) + : serverFactory.createTcpServer(connectParams); + servers.add(serverFuture.get()); + openDevices.add(port); + } catch (UnsupportedConfigurationException | InterruptedException | ExecutionException e) { + LOG.error("Could not start {} simulated device on port {}", proto, port, e); break; } - final InetSocketAddress address = getAddress(configuration.getIp(), currentPort); - - final ChannelFuture server; - if (configuration.isSsh()) { - final InetSocketAddress bindingAddress = InetSocketAddress.createUnresolved("0.0.0.0", currentPort); - final LocalAddress tcpLocalAddress = new LocalAddress(address.toString()); - - server = dispatcher.createLocalServer(tcpLocalAddress); - try { - final SshProxyServer sshServer = new SshProxyServer( - minaTimerExecutor, nettyThreadgroup, group); - sshServer.bind(getSshConfiguration(bindingAddress, tcpLocalAddress, keyPairProvider)); - sshWrappers.add(sshServer); - } catch (final BindException e) { - LOG.warn("Cannot start simulated device on {}, port already in use. Skipping.", address); - // Close local server and continue - server.cancel(true); - if (server.isDone()) { - server.channel().close(); - } - continue; - } catch (final IOException e) { - LOG.warn("Cannot start simulated device on {} due to IOException.", address, e); - break; - } finally { - currentPort++; - } - - try { - server.get(); - } catch (final InterruptedException e) { - throw new IllegalStateException("Interrupted while waiting for server", 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 IllegalStateException("Interrupted while waiting for server", e); - } catch (final ExecutionException e) { - LOG.warn("Cannot start tcp simulated device on {}, skipping", address, e); - continue; - } - - LOG.debug("Simulated TCP device started on {}", server.channel().localAddress()); - } - - devicesChannels.add(server.channel()); - openDevices.add(currentPort - 1); } + final var first = openDevices.get(0); + final var last = openDevices.isEmpty() ? null : openDevices.get(openDevices.size() - 1); if (openDevices.size() == configuration.getDeviceCount()) { - LOG.info("All simulated devices started successfully from port {} to {}", - configuration.getStartingPort(), currentPort - 1); - } else if (openDevices.size() == 0) { + LOG.info("All simulated devices started successfully from port {} to {}", first, last); + } else if (openDevices.isEmpty()) { LOG.warn("No simulated devices started."); } else { - LOG.warn("Not all simulated devices started successfully. Started devices ar on ports {}", openDevices); + LOG.warn("Not all simulated devices started successfully. Started devices are on ports {} to {}", + first, last); } - return openDevices; } - private SshProxyServerConfiguration getSshConfiguration(final InetSocketAddress bindingAddress, - final LocalAddress tcpLocalAddress, final KeyPairProvider keyPairProvider) { - return new SshProxyServerConfigurationBuilder() - .setBindingAddress(bindingAddress) - .setLocalAddress(tcpLocalAddress) - .setAuthenticator(configuration.getAuthProvider()) - .setPublickeyAuthenticator(configuration.getPublickeyAuthenticator()) - .setKeyPairProvider(keyPairProvider) - .setIdleTimeout(Integer.MAX_VALUE) - .createSshProxyServerConfiguration(); + private static ServerFactoryManagerConfigurator createServerFactoryManagerConfigurator( + final Configuration configuration) { + final var authProvider = configuration.getAuthProvider(); + final var publicKeyAuthenticator = configuration.getPublickeyAuthenticator(); + return factoryManager -> { + final var authFactoriesListBuilder = ImmutableList.builder(); + authFactoriesListBuilder.add(new UserAuthPasswordFactory()); + factoryManager.setPasswordAuthenticator( + (usr, pass, session) -> authProvider.authenticated(usr, pass)); + if (publicKeyAuthenticator != null) { + final var factory = new UserAuthPublicKeyFactory(); + factory.setSignatureFactories(factoryManager.getSignatureFactories()); + authFactoriesListBuilder.add(factory); + factoryManager.setPublickeyAuthenticator(publicKeyAuthenticator); + } + factoryManager.setUserAuthFactories(authFactoriesListBuilder.build()); + factoryManager.setKeyPairProvider(new VirtualKeyPairProvider()); + }; } private Set parseSchemasToModuleCapabilities(final SharedSchemaRepository consumer) { @@ -371,28 +321,44 @@ public class NetconfDeviceSimulator implements Closeable { PotentialSchemaSource.Costs.IMMEDIATE.getValue())); } - private static InetSocketAddress getAddress(final String ip, final int port) { + private static IpAddress getIpAddress(final Configuration configuration) { try { - return new InetSocketAddress(Inet4Address.getByName(ip), port); + return IetfInetUtil.ipAddressFor(InetAddress.getByName(configuration.getIp())); } catch (final UnknownHostException e) { - throw new IllegalArgumentException("Cannot resolve address " + ip, e); + throw new IllegalArgumentException("Cannot resolve address " + configuration.getIp(), e); + } + } + + private static int getStartingPort(final Configuration configuration) { + final int startingPort = configuration.getStartingPort(); + if (startingPort > 0 && startingPort < 65536) { + return startingPort; + } + // find available port + try { + final var socket = new ServerSocket(0); + final int port = socket.getLocalPort(); + socket.close(); + return port; + } catch (IOException e) { + throw new IllegalStateException("Cannot find available port", e); } } + private static TcpServerGrouping connectionParams(final IpAddress address, final int port) { + return new TcpServerParametersBuilder().setLocalAddress(address) + .setLocalPort(new PortNumber(Uint16.valueOf(port))).build(); + } + @Override public void close() { - for (final SshProxyServer sshWrapper : sshWrappers) { + for (final var server : servers) { try { - sshWrapper.close(); - } catch (final IOException e) { - LOG.debug("Wrapper {} failed to close", sshWrapper, e); + server.shutdown().get(); + } catch (InterruptedException | ExecutionException e) { + LOG.debug("Exception on simulated device shutdown", e); } } - for (final Channel deviceCh : devicesChannels) { - deviceCh.close(); - } - nettyThreadgroup.shutdownGracefully(); - minaTimerExecutor.shutdownNow(); - nioExecutor.shutdownNow(); + sshTransportStackFactory.close(); } } diff --git a/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/LogPropertyCatcher.java b/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/LogPropertyCatcher.java deleted file mode 100644 index ff934a3d96..0000000000 --- a/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/LogPropertyCatcher.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2019 Ericsson Software Technology AB. 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.test.tool; - -import static org.slf4j.Logger.ROOT_LOGGER_NAME; - -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.AppenderBase; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.junit.rules.ExternalResource; -import org.slf4j.LoggerFactory; - -/** - * JUnit Rule that captures a pattern-matching property from log messages. Every time a log messages matches a given - * pattern, the last capturing group will be saved, and can be later retrieved - * via {@link LogPropertyCatcher#getLastValue()}. - * - * @see Rules · junit-team/junit4 Wiki - */ -class LogPropertyCatcher extends ExternalResource { - - private final ListAppender appender; - - LogPropertyCatcher(Pattern pattern) { - this.appender = new ListAppender(pattern); - } - - @Override - protected void before() { - Logger rootLogger = (Logger) LoggerFactory.getLogger(ROOT_LOGGER_NAME); - appender.clear(); - rootLogger.addAppender(appender); - appender.start(); - } - - @Override - protected void after() { - appender.stop(); - Logger rootLogger = (Logger) LoggerFactory.getLogger(ROOT_LOGGER_NAME); - rootLogger.detachAppender(appender); - } - - /** - * Retrieves the last captured property. - * - * @return The last value captured, or Optional.empty() if no log messages matched the pattern. - */ - Optional getLastValue() { - return Optional.ofNullable(appender.lastValue); - } - - private static final class ListAppender extends AppenderBase { - - private final Pattern pattern; - - private String lastValue = null; - - private ListAppender(Pattern pattern) { - this.pattern = pattern; - } - - protected void append(ILoggingEvent evt) { - String msg = evt.getFormattedMessage(); - Matcher matcher = pattern.matcher(msg); - if (matcher.find()) { - lastValue = matcher.group(matcher.groupCount()); - } - } - - void clear() { - this.lastValue = null; - } - } -} diff --git a/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/TestToolTest.java b/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/TestToolTest.java index d2eeb329ad..6145ec5dde 100644 --- a/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/TestToolTest.java +++ b/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/TestToolTest.java @@ -5,148 +5,149 @@ * 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.test.tool; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol.SSH; +import static org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol.TCP; import static org.xmlunit.assertj.XmlAssert.assertThat; import com.google.common.collect.ImmutableMap; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.util.HashedWheelTimer; -import io.netty.util.concurrent.DefaultThreadFactory; import java.io.File; -import java.net.InetSocketAddress; +import java.net.InetAddress; +import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.opendaylight.netconf.api.messages.NetconfMessage; import org.opendaylight.netconf.api.xml.XmlUtil; import org.opendaylight.netconf.auth.AuthProvider; -import org.opendaylight.netconf.client.NetconfClientDispatcher; -import org.opendaylight.netconf.client.NetconfClientDispatcherImpl; +import org.opendaylight.netconf.client.NetconfClientFactory; +import org.opendaylight.netconf.client.NetconfClientFactoryImpl; import org.opendaylight.netconf.client.NetconfClientSession; import org.opendaylight.netconf.client.NetconfClientSessionListener; import org.opendaylight.netconf.client.SimpleNetconfClientSessionListener; import org.opendaylight.netconf.client.conf.NetconfClientConfiguration; import org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol; import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder; -import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.LoginPasswordHandler; import org.opendaylight.netconf.test.tool.config.Configuration; import org.opendaylight.netconf.test.tool.config.ConfigurationBuilder; -import org.opendaylight.netconf.test.tool.config.YangResource; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.password.grouping.password.type.CleartextPasswordBuilder; +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.IetfInetUtil; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev230417.netconf.client.initiate.stack.grouping.transport.ssh.ssh.TcpClientParametersBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev230417.netconf.client.listen.stack.grouping.transport.ssh.ssh.SshClientParametersBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentityBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.client.identity.PasswordBuilder; +import org.opendaylight.yangtools.yang.common.Uint16; import org.w3c.dom.Document; -@SuppressWarnings("SameParameterValue") public class TestToolTest { - - private static final long RECEIVE_TIMEOUT_MS = 5_000; + private static final long RESPONSE_TIMEOUT_MS = 30_000; private static final int RANDOM_PORT = 0; - - private static final User ADMIN_USER = new User("admin", "admin"); + private static final String USERNAME = "username"; + private static final String PASSWORD = "pa$$W0rd"; + private static final AuthProvider AUTH_PROVIDER = (user, passw) -> USERNAME.equals(user) && PASSWORD.equals(passw); private static final File CUSTOM_RPC_CONFIG = new File("src/test/resources/customrpc.xml"); - private static final Configuration SSH_SIMULATOR_CONFIG = getSimulatorConfig(NetconfClientProtocol.SSH, - ADMIN_USER); - private static final Configuration TCP_SIMULATOR_CONFIG = getSimulatorConfig(NetconfClientProtocol.SSH, - ADMIN_USER); - - private static NioEventLoopGroup nettyGroup; - private static NetconfClientDispatcherImpl dispatcher; - - - @Rule - public LogPropertyCatcher logPropertyCatcher = - new LogPropertyCatcher(Pattern.compile("(start\\(\\) listen on auto-allocated port=" - + "|Simulated TCP device started on (/0:0:0:0:0:0:0:0|/0.0.0.0):)(\\d+)")); - - private static final String XML_REQUEST_RFC7950_SECTION_4_2_9 = "\n" - + " \n" - + " example-fw-2.3\n" - + " \n" - + " "; - private static final String EXPECTED_XML_RESPONSE_RFC7950_SECTION_4_2_9 = "\n" - + " \n" - + " The image example-fw-2.3 is being installed.\n" - + " \n" - + " "; - private static final String XML_REQUEST_RFC7950_SECTION_7_15_3 = "\n" - + " \n" - + " \n" - + " apache-1\n" - + " \n" - + " 2014-07-29T13:42:00Z\n" - + " \n" - + " \n" - + " \n" - + " "; - private static final String EXPECTED_XML_RESPONSE_RFC7950_SECTION_7_15_3 = "\n" - + " \n" - + " 2014-07-29T13:42:12Z\n" - + " \n" - + ""; + + private static final String RFC7950_4_2_9_REQUEST = """ + + + example-fw-2.3 + + """; + private static final String RFC7950_4_2_9_RESPONSE = """ + + + The image example-fw-2.3 is being installed. + + """; + private static final String RFC7950_7_15_3_REQUEST = """ + + + + apache-1 + + 2014-07-29T13:42:00Z + + + + """; + private static final String RFC7950_7_15_3_RESPONSE = """ + + + 2014-07-29T13:42:12Z + + """; + private static final String GET_SCHEMAS_REQUEST = """ + + + + + + + + + """; private static final Map PREFIX_2_URI = ImmutableMap.of( "base10", "urn:ietf:params:xml:ns:netconf:base:1.0", "ncmon", "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring" ); - @BeforeClass - public static void setUpClass() { - HashedWheelTimer hashedWheelTimer = new HashedWheelTimer(); - nettyGroup = new NioEventLoopGroup(1, new DefaultThreadFactory(NetconfClientDispatcher.class)); - dispatcher = new NetconfClientDispatcherImpl(nettyGroup, nettyGroup, hashedWheelTimer); + private static NetconfClientFactory clientFactory; + private static NetconfDeviceSimulator tcpDeviceSimulator; + private static NetconfDeviceSimulator sshDeviceSimulator; + private static int tcpDevicePort; + private static int sshDevicePort; + + @BeforeAll + static void beforeAll() { + clientFactory = new NetconfClientFactoryImpl(); + tcpDeviceSimulator = new NetconfDeviceSimulator(getSimulatorConfig(TCP)); + tcpDevicePort = startSimulator(tcpDeviceSimulator); + sshDeviceSimulator = new NetconfDeviceSimulator(getSimulatorConfig(SSH)); + sshDevicePort = startSimulator(sshDeviceSimulator); } - @AfterClass - public static void cleanUpClass() - throws InterruptedException { - nettyGroup.shutdownGracefully().sync(); + @AfterAll + public static void afterAll() throws Exception { + stopSimulator(tcpDeviceSimulator); + tcpDeviceSimulator = null; + stopSimulator(sshDeviceSimulator); + sshDeviceSimulator = null; + clientFactory.close(); } - @Test - public void customRpcOverSsh() - throws Exception { - Document docResponse = invokeRpc(SSH_SIMULATOR_CONFIG, XML_REQUEST_RFC7950_SECTION_7_15_3); - assertThat(docResponse) - .and(EXPECTED_XML_RESPONSE_RFC7950_SECTION_7_15_3) - .ignoreWhitespace() - .areIdentical(); + @ParameterizedTest(name = "Custom RPC -- RFC7950 {0}") + @MethodSource("customRpcArgs") + void customRpc(final String ignoredTestDesc, final NetconfClientProtocol protocol, final String requestXml, + final String responseXml) throws Exception { + final var docResponse = sendRequest(protocol, requestXml); + assertThat(docResponse).and(responseXml).ignoreWhitespace().areIdentical(); } - @Test - public void customRpcOverTcp() - throws Exception { - Document docResponse = invokeRpc(TCP_SIMULATOR_CONFIG, XML_REQUEST_RFC7950_SECTION_4_2_9); - assertThat(docResponse) - .and(EXPECTED_XML_RESPONSE_RFC7950_SECTION_4_2_9) - .ignoreWhitespace() - .areIdentical(); + private static Stream customRpcArgs() { + return Stream.of( + // # test descriptor, protocol, request, expected response + Arguments.of("#7.15.3 @TCP", TCP, RFC7950_7_15_3_REQUEST, RFC7950_7_15_3_RESPONSE), + Arguments.of("#7.15.3 @SSH", SSH, RFC7950_7_15_3_REQUEST, RFC7950_7_15_3_RESPONSE), + Arguments.of("#4.2.9 @TCP", TCP, RFC7950_4_2_9_REQUEST, RFC7950_4_2_9_RESPONSE), + Arguments.of("#4.2.9 @SSH", SSH, RFC7950_4_2_9_REQUEST, RFC7950_4_2_9_RESPONSE) + ); } - @Test - public void shouldSupportGetSchema() - throws Exception { - String getSchema = "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""; - Document docResponse = invokeRpc(TCP_SIMULATOR_CONFIG, getSchema); - Set expectedYangResources = Configuration.DEFAULT_YANG_RESOURCES; + @ParameterizedTest(name = "Get Schemas @{0}") + @MethodSource("getSchemasArgs") + void getSchemas(final NetconfClientProtocol protocol) throws Exception { + final var docResponse = sendRequest(protocol, GET_SCHEMAS_REQUEST); + final var expectedYangResources = Configuration.DEFAULT_YANG_RESOURCES; assertEquals(4, expectedYangResources.size()); assertThat(docResponse) .withNamespaceContext(PREFIX_2_URI) @@ -154,89 +155,73 @@ public class TestToolTest { .isEqualTo(expectedYangResources.size()); } - private Document invokeRpc(final Configuration simulatorConfig, final String xmlRequest) + private static List getSchemasArgs() { + return List.of(SSH, TCP); + } + + private static Document sendRequest(final NetconfClientProtocol protocol, final String xml) throws Exception { - // GIVEN - int localPort = launchSimulator(simulatorConfig); - SimpleNetconfClientSessionListener sessionListener = new SimpleNetconfClientSessionListener(); - NetconfClientConfiguration clientConfig = getClientConfig("localhost", localPort, - simulatorConfig, sessionListener); - Document docRequest = XmlUtil.readXmlToDocument(xmlRequest); - NetconfMessage request = new NetconfMessage(docRequest); - - // WHEN + final var sessionListener = new SimpleNetconfClientSessionListener(); + final int port = SSH == protocol ? sshDevicePort : tcpDevicePort; + final var clientConfig = getClientConfig(port, protocol, sessionListener); + final var request = new NetconfMessage(XmlUtil.readXmlToDocument(xml)); NetconfMessage response; - try (NetconfClientSession ignored = dispatcher.createClient(clientConfig).get()) { - response = sessionListener.sendRequest(request) - .get(RECEIVE_TIMEOUT_MS, TimeUnit.MILLISECONDS); + try (NetconfClientSession ignored = clientFactory.createClient(clientConfig) + .get(RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + response = sessionListener.sendRequest(request).get(RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS); } - - // THEN assertNotNull(response); return response.getDocument(); } - private static final ConcurrentHashMap CACHED_SIMULATORS = new ConcurrentHashMap<>(); - /** - * Retrieves a previously launched simulator or launches a new one using the given configuration. + * Runs a simulator. * - * @param configuration The simulator configuration. + * @param simulator simulator instance * @return The TCP port number to access the launched simulator. */ - private int launchSimulator(final Configuration configuration) { - return CACHED_SIMULATORS.computeIfAbsent(configuration, cfg -> { - NetconfDeviceSimulator simulator = new NetconfDeviceSimulator(cfg); - simulator.start(); - return logPropertyCatcher.getLastValue() - .map(Integer::parseInt) - .orElseThrow(() -> new IllegalArgumentException("Unable to capture auto-allocated port from log")); - }); + private static int startSimulator(final NetconfDeviceSimulator simulator) { + final var openDevices = simulator.start(); + if (openDevices != null && !openDevices.isEmpty()) { + return openDevices.get(0); + } + throw new IllegalStateException("Could not start device simulator"); + } + + private static void stopSimulator(final NetconfDeviceSimulator simulator) { + if (simulator != null) { + simulator.close(); + } } @SuppressWarnings("deprecation") - private static Configuration getSimulatorConfig(final NetconfClientProtocol protocol, final User user) { + private static Configuration getSimulatorConfig(final NetconfClientProtocol protocol) { return new ConfigurationBuilder() .setStartingPort(RANDOM_PORT) + .setDeviceCount(1) .setRpcConfigFile(CUSTOM_RPC_CONFIG) - .setSsh(protocol == NetconfClientProtocol.SSH) - .setAuthProvider(new InMemoryAuthenticationProvider(user)) + .setSsh(SSH == protocol) + .setAuthProvider(AUTH_PROVIDER) .build(); } - private static NetconfClientConfiguration getClientConfig(final String host, final int port, - final Configuration simulatorConfig, - final NetconfClientSessionListener sessionListener) { - User user = ((InMemoryAuthenticationProvider) simulatorConfig.getAuthProvider()).user; + private static NetconfClientConfiguration getClientConfig(final int port, + final NetconfClientProtocol protocol, final NetconfClientSessionListener sessionListener) { return NetconfClientConfigurationBuilder.create() - .withAddress(new InetSocketAddress(host, port)) + .withTcpParameters( + new TcpClientParametersBuilder() + .setRemoteAddress(new Host(IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress()))) + .setRemotePort(new PortNumber(Uint16.valueOf(port))) + .build()) + .withSshParameters( + new SshClientParametersBuilder() + .setClientIdentity(new ClientIdentityBuilder() + .setUsername(USERNAME) + .setPassword(new PasswordBuilder().setPasswordType( + new CleartextPasswordBuilder().setCleartextPassword(PASSWORD).build() + ).build()).build()).build()) .withSessionListener(sessionListener) - .withProtocol(simulatorConfig.isSsh() ? NetconfClientProtocol.SSH : NetconfClientProtocol.TCP) - .withAuthHandler(new LoginPasswordHandler(user.username, user.password)) + .withProtocol(protocol) .build(); } - - private static final class User { - private final String username; - private final String password; - - private User(final String username, final String password) { - this.username = username; - this.password = password; - } - } - - private static final class InMemoryAuthenticationProvider implements AuthProvider { - - private final User user; - - private InMemoryAuthenticationProvider(final User user) { - this.user = user; - } - - @Override - public boolean authenticated(final String username, final String password) { - return user.username.equals(username) && user.password.equals(password); - } - } } -- 2.36.6