| /* |
| * Copyright 2017-present Open Networking Foundation |
| * |
| * 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.d.config.sync.impl.netconf; |
| |
| import static org.hamcrest.Matchers.hasItem; |
| import static org.hamcrest.Matchers.hasSize; |
| import static org.hamcrest.Matchers.is; |
| import static org.hamcrest.Matchers.stringContainsInOrder; |
| import static org.junit.Assert.*; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URI; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.BiFunction; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.commons.io.input.ReaderInputStream; |
| import org.hamcrest.Matchers; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.onlab.util.XmlString; |
| import org.onosproject.d.config.sync.DeviceConfigSynchronizationProviderService; |
| import org.onosproject.d.config.sync.impl.netconf.NetconfDeviceConfigSynchronizerComponent.NetconfContext; |
| import org.onosproject.d.config.sync.operation.SetRequest; |
| import org.onosproject.d.config.sync.operation.SetResponse; |
| import org.onosproject.d.config.sync.operation.SetResponse.Code; |
| import org.onosproject.net.DeviceId; |
| import org.onosproject.net.provider.ProviderId; |
| import org.onosproject.netconf.NetconfController; |
| import org.onosproject.netconf.NetconfException; |
| import org.onosproject.netconf.NetconfSession; |
| import org.onosproject.netconf.NetconfSessionAdapter; |
| import org.onosproject.yang.model.DataNode; |
| import org.onosproject.yang.model.DataNode.Type; |
| import org.onosproject.yang.model.InnerNode; |
| import org.onosproject.yang.model.LeafNode; |
| import org.onosproject.yang.model.ResourceData; |
| import org.onosproject.yang.model.ResourceId; |
| import org.onosproject.yang.model.SchemaContextProvider; |
| import org.onosproject.yang.runtime.AnnotatedNodeInfo; |
| import org.onosproject.yang.runtime.CompositeData; |
| import org.onosproject.yang.runtime.CompositeStream; |
| import org.onosproject.yang.runtime.DefaultAnnotatedNodeInfo; |
| import org.onosproject.yang.runtime.DefaultAnnotation; |
| import org.onosproject.yang.runtime.DefaultCompositeStream; |
| import org.onosproject.yang.runtime.RuntimeContext; |
| import org.onosproject.yang.runtime.YangRuntimeService; |
| import com.google.common.io.CharSource; |
| |
| public class NetconfDeviceConfigSynchronizerProviderTest { |
| |
| private static final ProviderId PID = new ProviderId("netconf", "test"); |
| private static final DeviceId DID = DeviceId.deviceId("netconf:testDevice"); |
| |
| private static final String XMLNS_XC = "xmlns:xc"; |
| private static final String NETCONF_1_0_BASE_NAMESPACE = |
| "urn:ietf:params:xml:ns:netconf:base:1.0"; |
| |
| private static final DefaultAnnotation XC_ANNOTATION = |
| new DefaultAnnotation(XMLNS_XC, NETCONF_1_0_BASE_NAMESPACE); |
| |
| private static final DefaultAnnotation AN_XC_REPLACE_OPERATION = |
| new DefaultAnnotation("xc:operation", "replace"); |
| |
| private static final DefaultAnnotation AN_XC_REMOVE_OPERATION = |
| new DefaultAnnotation("xc:operation", "remove"); |
| |
| /** |
| * Yang namespace for test config data. |
| */ |
| private static final String TEST_NS = "testNS"; |
| |
| private static final ResourceId RID_INTERFACES = |
| ResourceId.builder().addBranchPointSchema("interfaces", TEST_NS).build(); |
| |
| private NetconfDeviceConfigSynchronizerProvider sut; |
| |
| private NetconfContext ncCtx; |
| |
| |
| // Set following accordingly to suite test scenario |
| NetconfSession testNcSession; |
| YangRuntimeService testYangRuntime; |
| |
| |
| @Before |
| public void setUp() throws Exception { |
| |
| ncCtx = new TestNetconfContext(); |
| |
| sut = new NetconfDeviceConfigSynchronizerProvider(PID, ncCtx) { |
| // overriding to avoid mocking whole NetconController and all that. |
| @Override |
| protected NetconfSession getNetconfSession(DeviceId deviceId) { |
| assertEquals(DID, deviceId); |
| return testNcSession; |
| } |
| }; |
| } |
| |
| @Test |
| public void testReplaceOperation() throws Exception { |
| // plug drivers with assertions |
| testYangRuntime = onEncode((data, context) -> { |
| assertEquals("xml", context.getDataFormat()); |
| assertThat(context.getProtocolAnnotations(), hasItem(XC_ANNOTATION)); |
| |
| // assert CompositeData |
| ResourceData rData = data.resourceData(); |
| List<AnnotatedNodeInfo> infos = data.annotatedNodesInfo(); |
| |
| ResourceId interfacesRid = RID_INTERFACES; |
| AnnotatedNodeInfo intfsAnnot = DefaultAnnotatedNodeInfo.builder() |
| .resourceId(interfacesRid) |
| .addAnnotation(AN_XC_REPLACE_OPERATION) |
| .build(); |
| assertThat("interfaces has replace operation", infos, hasItem(intfsAnnot)); |
| |
| // assertion for ResourceData. |
| assertEquals(RID_INTERFACES, rData.resourceId()); |
| assertThat("has 1 child", rData.dataNodes(), hasSize(1)); |
| assertThat("which is interface", |
| rData.dataNodes().get(0).key().schemaId().name(), |
| is("interface")); |
| // todo: assert the rest of the tree if it make sense. |
| |
| // FIXME it's unclear what URI is expected here |
| String id = URI.create("netconf:testDevice").toString(); |
| |
| String inXml = deviceConfigAsXml("replace"); |
| |
| return toCompositeStream(id, inXml); |
| }); |
| testNcSession = new TestEditNetconfSession(); |
| |
| |
| // building test data |
| ResourceId interfacesId = RID_INTERFACES; |
| DataNode interfaces = deviceConfigNode(); |
| SetRequest request = SetRequest.builder() |
| .replace(interfacesId, interfaces) |
| .build(); |
| |
| // test start |
| CompletableFuture<SetResponse> f = sut.setConfiguration(DID, request); |
| SetResponse response = f.get(5, TimeUnit.MINUTES); |
| |
| assertEquals(Code.OK, response.code()); |
| assertEquals(request.subjects(), response.subjects()); |
| } |
| |
| |
| @Test |
| public void testDeleteOperation() throws Exception { |
| // plug drivers with assertions |
| testYangRuntime = onEncode((data, context) -> { |
| assertEquals("xml", context.getDataFormat()); |
| assertThat(context.getProtocolAnnotations(), hasItem(XC_ANNOTATION)); |
| |
| // assert CompositeData |
| ResourceData rData = data.resourceData(); |
| List<AnnotatedNodeInfo> infos = data.annotatedNodesInfo(); |
| |
| ResourceId interfacesRid = RID_INTERFACES; |
| AnnotatedNodeInfo intfsAnnot = DefaultAnnotatedNodeInfo.builder() |
| .resourceId(interfacesRid) |
| .addAnnotation(AN_XC_REMOVE_OPERATION) |
| .build(); |
| assertThat("interfaces has replace operation", infos, hasItem(intfsAnnot)); |
| |
| // assertion for ResourceData. |
| assertEquals(RID_INTERFACES, rData.resourceId()); |
| assertThat("has no child", rData.dataNodes(), hasSize(0)); |
| |
| // FIXME it's unclear what URI is expected here |
| String id = URI.create("netconf:testDevice").toString(); |
| |
| String inXml = deviceConfigAsXml("remove"); |
| |
| return toCompositeStream(id, inXml); |
| }); |
| testNcSession = new TestEditNetconfSession(); |
| |
| // building test data |
| ResourceId interfacesId = RID_INTERFACES; |
| SetRequest request = SetRequest.builder() |
| .delete(interfacesId) |
| .build(); |
| |
| // test start |
| CompletableFuture<SetResponse> f = sut.setConfiguration(DID, request); |
| |
| SetResponse response = f.get(5, TimeUnit.MINUTES); |
| assertEquals(Code.OK, response.code()); |
| assertEquals(request.subjects(), response.subjects()); |
| } |
| |
| /** |
| * DataNode for testing. |
| * |
| * <pre> |
| * +-interfaces |
| * | |
| * +- interface{intf-name="en0"} |
| * | |
| * +- speed = "10G" |
| * +- state = "up" |
| * |
| * </pre> |
| * @return DataNode |
| */ |
| private DataNode deviceConfigNode() { |
| InnerNode.Builder intfs = InnerNode.builder("interfaces", TEST_NS); |
| intfs.type(Type.SINGLE_INSTANCE_NODE); |
| InnerNode.Builder intf = intfs.createChildBuilder("interface", TEST_NS); |
| intf.type(Type.SINGLE_INSTANCE_LEAF_VALUE_NODE); |
| intf.addKeyLeaf("name", TEST_NS, "Ethernet0/0"); |
| LeafNode.Builder speed = intf.createChildBuilder("mtu", TEST_NS, "1500"); |
| speed.type(Type.SINGLE_INSTANCE_LEAF_VALUE_NODE); |
| |
| intf.addNode(speed.build()); |
| intfs.addNode(intf.build()); |
| return intfs.build(); |
| } |
| |
| /** |
| * {@link #deviceConfigNode()} as XML. |
| * |
| * @param operation xc:operation value on {@code interfaces} node |
| * @return XML |
| */ |
| private String deviceConfigAsXml(String operation) { |
| return "<interfaces xmlns=\"http://example.com/schema/1.2/config\"" |
| + " xc:operation=\"" + operation + "\">\n" + |
| " <interface>\n" + |
| " <name>Ethernet0/0</name>\n" + |
| " <mtu>1500</mtu>\n" + |
| " </interface>\n" + |
| "</interfaces>"; |
| } |
| |
| private String rpcReplyOk(int messageid) { |
| return "<rpc-reply message-id=\"" + messageid + "\"\n" + |
| " xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n" + |
| " <ok/>\n" + |
| "</rpc-reply>"; |
| } |
| |
| private int fetchMessageId(String request) { |
| int messageid; |
| Pattern msgId = Pattern.compile("message-id=['\"]([0-9]+)['\"]"); |
| Matcher matcher = msgId.matcher(request); |
| if (matcher.find()) { |
| messageid = Integer.parseInt(matcher.group(1)); |
| } else { |
| messageid = -1; |
| } |
| return messageid; |
| } |
| |
| |
| protected CompositeStream toCompositeStream(String id, String inXml) { |
| try { |
| InputStream xml = new ReaderInputStream( |
| CharSource.wrap(inXml) |
| .openStream()); |
| |
| return new DefaultCompositeStream(id, xml); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * Asserts that it received edit-config message and reply Ok. |
| */ |
| private class TestEditNetconfSession extends NetconfSessionAdapter { |
| @Override |
| public CompletableFuture<String> request(String request) |
| throws NetconfException { |
| System.out.println("TestEditNetconfSession received:"); |
| System.out.println(XmlString.prettifyXml(request)); |
| |
| // Extremely naive request rpc message check |
| assertThat(request, stringContainsInOrder(Arrays.asList( |
| "<rpc", |
| "<edit-config", |
| "<target", |
| "<config", |
| |
| "</config>", |
| "</edit-config>", |
| "</rpc>"))); |
| |
| assertThat("XML namespace decl exists", |
| request, Matchers.containsString("xmlns:xc")); |
| |
| assertThat("netconf operation exists", |
| request, Matchers.containsString("xc:operation")); |
| |
| return CompletableFuture.completedFuture(rpcReplyOk(fetchMessageId(request))); |
| } |
| } |
| |
| /** |
| * Creates mock YangRuntimeService. |
| * |
| * @param body to execute when {@link YangRuntimeService#encode(CompositeData, RuntimeContext)} was called. |
| * @return YangRuntimeService instance |
| */ |
| TestYangRuntimeService onEncode(BiFunction<CompositeData, RuntimeContext, CompositeStream> body) { |
| return new TestYangRuntimeService() { |
| @Override |
| public CompositeStream encode(CompositeData internal, |
| RuntimeContext context) { |
| return body.apply(internal, context); |
| } |
| }; |
| } |
| |
| private abstract class TestYangRuntimeService implements YangRuntimeService { |
| |
| @Override |
| public CompositeStream encode(CompositeData internal, |
| RuntimeContext context) { |
| fail("stub not implemented"); |
| return null; |
| } |
| @Override |
| public CompositeData decode(CompositeStream external, |
| RuntimeContext context) { |
| fail("stub not implemented"); |
| return null; |
| } |
| } |
| |
| private final class TestNetconfContext implements NetconfContext { |
| @Override |
| public DeviceConfigSynchronizationProviderService providerService() { |
| fail("Add stub driver as necessary"); |
| return null; |
| } |
| |
| @Override |
| public SchemaContextProvider schemaContextProvider() { |
| fail("Add stub driver as necessary"); |
| return null; |
| } |
| |
| @Override |
| public YangRuntimeService yangRuntime() { |
| return testYangRuntime; |
| } |
| |
| @Override |
| public NetconfController netconfController() { |
| fail("Add stub driver as necessary"); |
| return null; |
| } |
| } |
| |
| } |