blob: 28fad43f2caa084168f7a79d0d7cb4600209240e [file] [log] [blame]
/*
* 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 IllegalStateException(e);
}
}
/**
* Asserts that it received edit-config message and reply Ok.
*/
private class TestEditNetconfSession extends NetconfSessionAdapter {
@Override
public CompletableFuture<String> rpc(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;
}
}
}