/* * Copyright 2017-present Open Networking Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onosproject.netconf.ctl.impl; import com.google.common.collect.ImmutableList; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.server.Command; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.password.PasswordAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.session.ServerSession; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.onlab.junit.TestTools; import org.onlab.packet.Ip4Address; import org.onosproject.netconf.NetconfDeviceInfo; import org.onosproject.netconf.NetconfException; import org.onosproject.netconf.NetconfSession; import org.onosproject.netconf.TargetConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.regex.Pattern; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.*; import static org.onosproject.netconf.TargetConfig.CANDIDATE; import static org.onosproject.netconf.TargetConfig.RUNNING; /** * Unit tests for NetconfSession. * <p> * Sets up an SSH Server with Apache SSHD and connects to it using 2 clients * Truly verifies that the NETCONF flows are compliant with a NETCONF server. */ public class NetconfSessionMinaImplTest { private static final Logger log = LoggerFactory .getLogger(NetconfStreamThread.class); private static final int PORT_NUMBER = TestTools.findAvailablePort(50830); private static final String TEST_USERNAME = "netconf"; private static final String TEST_PASSWORD = "netconf123"; private static final String TEST_HOSTNAME = "127.0.0.1"; private static final String TEST_SERFILE = System.getProperty("java.io.tmpdir") + System.getProperty("file.separator") + "testkey.ser"; private static final String SAMPLE_REQUEST = "<some-yang-element xmlns=\"some-namespace\">" + "<some-child-element/>" + "</some-yang-element>"; private static final String EDIT_CONFIG_REQUEST = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rpc message-id=\"6\" " + "xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n" + "<edit-config>\n" + "<target><running/></target>\n" + "<config xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n" + "<some-yang-element xmlns=\"some-namespace\">" + "<some-child-element/></some-yang-element></config>\n" + "</edit-config>\n" + "</rpc>]]>]]>"; static final List<String> DEFAULT_CAPABILITIES = ImmutableList.<String>builder() .add("urn:ietf:params:netconf:base:1.0") .add("urn:ietf:params:netconf:base:1.1") .add("urn:ietf:params:netconf:capability:writable-running:1.0") .add("urn:ietf:params:netconf:capability:candidate:1.0") .add("urn:ietf:params:netconf:capability:startup:1.0") .add("urn:ietf:params:netconf:capability:rollback-on-error:1.0") .add("urn:ietf:params:netconf:capability:interleave:1.0") .add("urn:ietf:params:netconf:capability:notification:1.0") .add("urn:ietf:params:netconf:capability:validate:1.0") .add("urn:ietf:params:netconf:capability:validate:1.1") .build(); private static NetconfSession session1; private static NetconfSession session2; private static SshServer sshServerNetconf; @BeforeClass public static void setUp() throws Exception { sshServerNetconf = SshServer.setUpDefaultServer(); sshServerNetconf.setPasswordAuthenticator( new PasswordAuthenticator() { @Override public boolean authenticate( String username, String password, ServerSession session) { return TEST_USERNAME.equals(username) && TEST_PASSWORD.equals(password); } }); sshServerNetconf.setPort(PORT_NUMBER); SimpleGeneratorHostKeyProvider provider = new SimpleGeneratorHostKeyProvider(); provider.setFile(new File(TEST_SERFILE)); sshServerNetconf.setKeyPairProvider(provider); sshServerNetconf.setSubsystemFactories( Arrays.<NamedFactory<Command>>asList(new NetconfSshdTestSubsystem.Factory())); sshServerNetconf.open(); log.info("SSH Server opened on port {}", PORT_NUMBER); NetconfDeviceInfo deviceInfo = new NetconfDeviceInfo( TEST_USERNAME, TEST_PASSWORD, Ip4Address.valueOf(TEST_HOSTNAME), PORT_NUMBER); session1 = new NetconfSessionMinaImpl(deviceInfo); log.info("Started NETCONF Session {} with test SSHD server in Unit Test", session1.getSessionId()); assertTrue("Incorrect sessionId", !session1.getSessionId().equalsIgnoreCase("-1")); assertTrue("Incorrect sessionId", !session1.getSessionId().equalsIgnoreCase("0")); assertThat(session1.getDeviceCapabilitiesSet(), containsInAnyOrder(DEFAULT_CAPABILITIES.toArray())); session2 = new NetconfSessionMinaImpl(deviceInfo); log.info("Started NETCONF Session {} with test SSHD server in Unit Test", session2.getSessionId()); assertTrue("Incorrect sessionId", !session2.getSessionId().equalsIgnoreCase("-1")); assertTrue("Incorrect sessionId", !session2.getSessionId().equalsIgnoreCase("0")); assertThat(session2.getDeviceCapabilitiesSet(), containsInAnyOrder(DEFAULT_CAPABILITIES.toArray())); } @AfterClass public static void tearDown() throws Exception { if (session1 != null) { session1.close(); } if (session2 != null) { session2.close(); } sshServerNetconf.stop(); } @Test public void testEditConfigRequest() { log.info("Starting edit-config async"); assertNotNull("Incorrect sessionId", session1.getSessionId()); try { assertTrue("NETCONF edit-config command failed", session1.editConfig(TargetConfig.RUNNING.toString(), null, SAMPLE_REQUEST)); } catch (NetconfException e) { e.printStackTrace(); fail("NETCONF edit-config test failed: " + e.getMessage()); } log.info("Finishing edit-config async"); } @Test public void testEditConfigRequestWithOnlyNewConfiguration() { log.info("Starting edit-config async"); assertNotNull("Incorrect sessionId", session1.getSessionId()); try { assertTrue("NETCONF edit-config command failed", session1.editConfig(EDIT_CONFIG_REQUEST)); } catch (NetconfException e) { e.printStackTrace(); fail("NETCONF edit-config test failed: " + e.getMessage()); } log.info("Finishing edit-config async"); } @Test public void testDeleteConfigRequestWithRunningTargetConfiguration() { log.info("Starting delete-config async"); assertNotNull("Incorrect sessionId", session1.getSessionId()); try { assertFalse("NETCONF delete-config command failed", session1.deleteConfig(TargetConfig.RUNNING)); } catch (NetconfException e) { e.printStackTrace(); fail("NETCONF delete-config test failed: " + e.getMessage()); } log.info("Finishing delete-config async"); } @Test public void testCopyConfigRequest() { log.info("Starting copy-config async"); assertNotNull("Incorrect sessionId", session1.getSessionId()); try { assertTrue("NETCONF copy-config command failed", session1.copyConfig(TargetConfig.RUNNING.toString(), "candidate")); } catch (NetconfException e) { e.printStackTrace(); fail("NETCONF edit-config test failed: " + e.getMessage()); } log.info("Finishing copy-config async"); } @Test public void testGetConfigRequest() { log.info("Starting get-config async"); assertNotNull("Incorrect sessionId", session1.getSessionId()); try { assertTrue("NETCONF get-config running command failed. ", GET_REPLY_PATTERN.matcher(session1.getConfig(RUNNING, SAMPLE_REQUEST)).matches()); assertTrue("NETCONF get-config candidate command failed. ", GET_REPLY_PATTERN.matcher(session1.getConfig(CANDIDATE, SAMPLE_REQUEST)).matches()); } catch (NetconfException e) { e.printStackTrace(); fail("NETCONF get-config test failed: " + e.getMessage()); } log.info("Finishing get-config async"); } @Test public void testGetRequest() { log.info("Starting get async"); assertNotNull("Incorrect sessionId", session1.getSessionId()); try { assertTrue("NETCONF get running command failed. ", GET_REPLY_PATTERN.matcher(session1.get(SAMPLE_REQUEST, null)).matches()); } catch (NetconfException e) { e.printStackTrace(); fail("NETCONF get test failed: " + e.getMessage()); } log.info("Finishing get async"); } @Test public void testLockRequest() { log.info("Starting lock async"); assertNotNull("Incorrect sessionId", session1.getSessionId()); try { assertTrue("NETCONF lock request failed", session1.lock()); } catch (NetconfException e) { e.printStackTrace(); fail("NETCONF lock test failed: " + e.getMessage()); } log.info("Finishing lock async"); } @Test public void testUnLockRequest() { log.info("Starting unlock async"); assertNotNull("Incorrect sessionId", session1.getSessionId()); try { assertTrue("NETCONF unlock request failed", session1.unlock()); } catch (NetconfException e) { e.printStackTrace(); fail("NETCONF unlock test failed: " + e.getMessage()); } log.info("Finishing unlock async"); } @Test public void testConcurrentSameSessionAccess() throws InterruptedException { NCCopyConfigCallable testCopyConfig1 = new NCCopyConfigCallable(session1, RUNNING, "candidate"); NCCopyConfigCallable testCopyConfig2 = new NCCopyConfigCallable(session1, RUNNING, "startup"); FutureTask<Boolean> futureCopyConfig1 = new FutureTask<>(testCopyConfig1); FutureTask<Boolean> futureCopyConfig2 = new FutureTask<>(testCopyConfig2); ExecutorService executor = Executors.newFixedThreadPool(2); log.info("Starting concurrent execution of copy-config through same session"); executor.execute(futureCopyConfig1); executor.execute(futureCopyConfig2); int count = 0; while (count < 10) { if (futureCopyConfig1.isDone() && futureCopyConfig2.isDone()) { executor.shutdown(); log.info("Finished concurrent same session execution"); return; } Thread.sleep(100L); count++; } fail("NETCONF test failed to complete."); } @Test public void test2SessionAccess() throws InterruptedException { NCCopyConfigCallable testCopySession1 = new NCCopyConfigCallable(session1, RUNNING, "candidate"); NCCopyConfigCallable testCopySession2 = new NCCopyConfigCallable(session2, RUNNING, "candidate"); FutureTask<Boolean> futureCopySession1 = new FutureTask<>(testCopySession1); FutureTask<Boolean> futureCopySession2 = new FutureTask<>(testCopySession2); ExecutorService executor = Executors.newFixedThreadPool(2); log.info("Starting concurrent execution of copy-config through 2 different sessions"); executor.execute(futureCopySession1); executor.execute(futureCopySession2); int count = 0; while (count < 10) { if (futureCopySession1.isDone() && futureCopySession2.isDone()) { executor.shutdown(); log.info("Finished concurrent 2 session execution"); return; } Thread.sleep(100L); count++; } fail("NETCONF test failed to complete."); } public static String getTestHelloReply(Optional<Long> sessionId) { return getTestHelloReply(DEFAULT_CAPABILITIES, sessionId); } public static String getTestHelloReply(Collection<String> capabilities, Optional<Long> sessionId) { StringBuffer sb = new StringBuffer(); sb.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">"); sb.append("<capabilities>"); capabilities.forEach(capability -> { sb.append("<capability>").append(capability).append("</capability>"); }); sb.append("</capabilities>"); if (sessionId.isPresent()) { sb.append("<session-id>"); sb.append(sessionId.get().toString()); sb.append("</session-id>"); } sb.append("</hello>"); return sb.toString(); } public static String getOkReply(Optional<Integer> messageId) { StringBuffer sb = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); sb.append("<rpc-reply xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" "); if (messageId.isPresent()) { sb.append("message-id=\""); sb.append(String.valueOf(messageId.get())); sb.append("\">"); } sb.append("<ok/>"); sb.append("</rpc-reply>"); return sb.toString(); } public static String getGetReply(Optional<Integer> messageId) { StringBuffer sb = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); sb.append("<rpc-reply xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" "); if (messageId.isPresent()) { sb.append("message-id=\""); sb.append(String.valueOf(messageId.get())); sb.append("\">"); } sb.append("<data>\n"); sb.append(SAMPLE_REQUEST); sb.append("</data>\n"); sb.append("</rpc-reply>"); return sb.toString(); } public static final Pattern HELLO_REQ_PATTERN = Pattern.compile("(<\\?xml).*" + "(<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">)\\R?" + "( *)(<capabilities>)\\R?" + "( *)(<capability>urn:ietf:params:netconf:base:1.0</capability>)\\R?" + "( *)(</capabilities>)\\R?" + "(</hello>)\\R? *", Pattern.DOTALL); public static final Pattern EDIT_CONFIG_REQ_PATTERN = Pattern.compile("(<\\?xml).*" + "(<rpc message-id=\")[0-9]*(\") *(xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">)\\R?" + "(<edit-config>)\\R?" + "(<target>\\R?((<" + TargetConfig.CANDIDATE.toString() + "/>)|" + "(<" + TargetConfig.RUNNING.toString() + "/>)|" + "(<" + TargetConfig.STARTUP.toString() + "/>))\\R?</target>)\\R?" + "(<config xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\">)\\R?" + ".*" + "(</config>)\\R?(</edit-config>)\\R?(</rpc>)\\R?", Pattern.DOTALL); public static final Pattern LOCK_REQ_PATTERN = Pattern.compile("(<\\?xml).*" + "(<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" " + "message-id=\")[0-9]*(\">)\\R?" + "(<lock>)\\R?" + "(<target>\\R?((<" + TargetConfig.CANDIDATE.toString() + "/>)|" + "(<" + TargetConfig.RUNNING.toString() + "/>)|" + "(<" + TargetConfig.STARTUP.toString() + "/>))\\R?</target>)\\R?" + "(</lock>)\\R?(</rpc>)\\R?", Pattern.DOTALL); public static final Pattern UNLOCK_REQ_PATTERN = Pattern.compile("(<\\?xml).*" + "(<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" " + "message-id=\")[0-9]*(\">)\\R?" + "(<unlock>)\\R?" + "(<target>\\R?((<" + TargetConfig.CANDIDATE.toString() + "/>)|" + "(<" + TargetConfig.RUNNING.toString() + "/>)|" + "(<" + TargetConfig.STARTUP.toString() + "/>))\\R?</target>)\\R?" + "(</unlock>)\\R?(</rpc>)\\R?", Pattern.DOTALL); public static final Pattern COPY_CONFIG_REQ_PATTERN = Pattern.compile("(<\\?xml).*" + "(<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" message-id=\")[0-9]*(\">)\\R?" + "(<copy-config>)\\R?" + "(<target>\\R?((<" + TargetConfig.CANDIDATE.toString() + "/>)|" + "(<" + TargetConfig.RUNNING.toString() + "/>)|" + "(<" + TargetConfig.STARTUP.toString() + "/>))\\R?</target>)\\R?" + "(<source>)\\R?(<config>)((" + TargetConfig.CANDIDATE.toString() + ")|(" + TargetConfig.RUNNING.toString() + ")|(" + TargetConfig.STARTUP.toString() + "))(</config>)\\R?(</source>)\\R?" + "(</copy-config>)\\R?(</rpc>)\\R?", Pattern.DOTALL); public static final Pattern GET_CONFIG_REQ_PATTERN = Pattern.compile("(<\\?xml).*" + "(<rpc message-id=\")[0-9]*(\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">)\\R?" + "(<get-config>)\\R?" + "(<source>)\\R?((<" + TargetConfig.CANDIDATE.toString() + "/>)|(<" + TargetConfig.RUNNING.toString() + "/>)|(<" + TargetConfig.STARTUP.toString() + "/>))\\R?(</source>)\\R?" + "(<filter type=\"subtree\">).*(</filter>)\\R?" + "(</get-config>)\\R?(</rpc>)\\R?", Pattern.DOTALL); public static final Pattern GET_REPLY_PATTERN = Pattern.compile("(<\\?xml).*" + "(<rpc-reply xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" message-id=\")[0-9]*(\">)\\R?" + "(<data>).*(</data>)\\R?" + "(</rpc-reply>)\\R?", Pattern.DOTALL); public static final Pattern GET_REQ_PATTERN = Pattern.compile("(<\\?xml).*" + "(<rpc message-id=\")[0-9]*(\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">)\\R?" + "(<get>)\\R?" + "(<filter type=\"subtree\">).*(</filter>)\\R?" + "(</get>)\\R?(</rpc>)\\R?", Pattern.DOTALL); public class NCCopyConfigCallable implements Callable<Boolean> { private NetconfSession session; private TargetConfig target; private String source; public NCCopyConfigCallable(NetconfSession session, TargetConfig target, String source) { this.session = session; this.target = target; this.source = source; } @Override public Boolean call() throws Exception { return session.copyConfig(target, source); } } }