Suitable for devices that do not provide module based capabilities in hello message (most likely do not support ietf-netconf-monitoring).
User can place yang files into cache/schema folder and set the capabilities for these modules into config attribute for netconf-connector.
Change-Id: I541ef08bb5c0328e293f9c732353282ffaf0474d
Signed-off-by: Maros Marsalek <mmarsale@cisco.com>
import java.io.File;
import java.io.InputStream;
import java.net.InetSocketAddress;
+import java.util.List;
import java.util.concurrent.ExecutorService;
+import org.opendaylight.controller.config.api.JmxAttributeValidationException;
import org.opendaylight.controller.netconf.client.NetconfClientDispatcher;
import org.opendaylight.controller.netconf.client.conf.NetconfClientConfiguration;
import org.opendaylight.controller.netconf.client.conf.NetconfReconnectingClientConfiguration;
import org.opendaylight.controller.sal.connect.api.RemoteDeviceHandler;
import org.opendaylight.controller.sal.connect.netconf.NetconfDevice;
import org.opendaylight.controller.sal.connect.netconf.listener.NetconfDeviceCommunicator;
+import org.opendaylight.controller.sal.connect.netconf.listener.NetconfSessionCapabilities;
import org.opendaylight.controller.sal.connect.netconf.sal.NetconfDeviceSalFacade;
import org.opendaylight.controller.sal.connect.util.RemoteDeviceId;
import org.opendaylight.controller.sal.core.api.Broker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.Optional;
+
/**
*
*/
private static AbstractCachingSchemaSourceProvider<String, InputStream> GLOBAL_NETCONF_SOURCE_PROVIDER = null;
private BundleContext bundleContext;
+ private Optional<NetconfSessionCapabilities> userCapabilities;
public NetconfConnectorModule(final org.opendaylight.controller.config.api.ModuleIdentifier identifier, final org.opendaylight.controller.config.api.DependencyResolver dependencyResolver) {
super(identifier, dependencyResolver);
checkNotNull(getPassword(), passwordJmxAttribute);
}
+ userCapabilities = getUserCapabilities();
+
}
- private boolean isHostAddressPresent(Host address) {
+ private boolean isHostAddressPresent(final Host address) {
return address.getDomainName() != null ||
address.getIpAddress() != null && (address.getIpAddress().getIpv4Address() != null || address.getIpAddress().getIpv6Address() != null);
}
final Broker domBroker = getDomRegistryDependency();
final BindingAwareBroker bindingBroker = getBindingRegistryDependency();
- final RemoteDeviceHandler salFacade = new NetconfDeviceSalFacade(id, domBroker, bindingBroker, bundleContext, globalProcessingExecutor);
+ final RemoteDeviceHandler<NetconfSessionCapabilities> salFacade
+ = new NetconfDeviceSalFacade(id, domBroker, bindingBroker, bundleContext, globalProcessingExecutor);
final NetconfDevice device =
NetconfDevice.createNetconfDevice(id, getGlobalNetconfSchemaProvider(), globalProcessingExecutor, salFacade);
- final NetconfDeviceCommunicator listener = new NetconfDeviceCommunicator(id, device);
+
+ final NetconfDeviceCommunicator listener = userCapabilities.isPresent() ?
+ new NetconfDeviceCommunicator(id, device, userCapabilities.get()) : new NetconfDeviceCommunicator(id, device);
+
final NetconfReconnectingClientConfiguration clientConfig = getClientConfig(listener);
final NetconfClientDispatcher dispatcher = getClientDispatcherDependency();
};
}
+ private Optional<NetconfSessionCapabilities> getUserCapabilities() {
+ if(getYangModuleCapabilities() == null) {
+ return Optional.absent();
+ }
+
+ final List<String> capabilities = getYangModuleCapabilities().getCapability();
+ if(capabilities == null || capabilities.isEmpty()) {
+ return Optional.absent();
+ }
+
+ final NetconfSessionCapabilities parsedOverrideCapabilities = NetconfSessionCapabilities.fromStrings(capabilities);
+ JmxAttributeValidationException.checkCondition(
+ parsedOverrideCapabilities.getNonModuleCaps().isEmpty(),
+ "Capabilities to override can only contain module based capabilities, non-module capabilities will be retrieved from the device," +
+ " configured non-module capabilities: " + parsedOverrideCapabilities.getNonModuleCaps(),
+ yangModuleCapabilitiesJmxAttribute);
+
+ return Optional.of(parsedOverrideCapabilities);
+ }
+
private synchronized AbstractCachingSchemaSourceProvider<String, InputStream> getGlobalNetconfSchemaProvider() {
if(GLOBAL_NETCONF_SOURCE_PROVIDER == null) {
final String storageFile = "cache/schema";
if(getAddress().getDomainName() != null) {
return new InetSocketAddress(getAddress().getDomainName().getValue(), getPort().getValue());
} else {
- IpAddress ipAddress = getAddress().getIpAddress();
- String ip = ipAddress.getIpv4Address() != null ? ipAddress.getIpv4Address().getValue() : ipAddress.getIpv6Address().getValue();
+ final IpAddress ipAddress = getAddress().getIpAddress();
+ final String ip = ipAddress.getIpv4Address() != null ? ipAddress.getIpv4Address().getValue() : ipAddress.getIpv6Address().getValue();
return new InetSocketAddress(ip, getPort().getValue());
}
}
// Unable to initialize device, set as disconnected
logger.error("{}: Initialization failed", id, t);
salFacade.onDeviceDisconnected();
+ // TODO ssh connection is still open if sal initialization fails
}
});
}
*/
package org.opendaylight.controller.sal.connect.netconf.listener;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-
import org.opendaylight.controller.netconf.api.NetconfDocumentedException;
import org.opendaylight.controller.netconf.api.NetconfMessage;
import org.opendaylight.controller.netconf.api.NetconfTerminationReason;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import io.netty.util.concurrent.Future;
-import io.netty.util.concurrent.FutureListener;
-
public class NetconfDeviceCommunicator implements NetconfClientSessionListener, RemoteDeviceCommunicator<NetconfMessage> {
private static final Logger logger = LoggerFactory.getLogger(NetconfDeviceCommunicator.class);
private final RemoteDevice<NetconfSessionCapabilities, NetconfMessage> remoteDevice;
+ private final Optional<NetconfSessionCapabilities> overrideNetconfCapabilities;
private final RemoteDeviceId id;
private final Lock sessionLock = new ReentrantLock();
+ private final Queue<Request> requests = new ArrayDeque<>();
+ private NetconfClientSession session;
+
+ public NetconfDeviceCommunicator(final RemoteDeviceId id, final RemoteDevice<NetconfSessionCapabilities, NetconfMessage> remoteDevice,
+ final NetconfSessionCapabilities netconfSessionCapabilities) {
+ this(id, remoteDevice, Optional.of(netconfSessionCapabilities));
+ }
+
public NetconfDeviceCommunicator(final RemoteDeviceId id,
- final RemoteDevice<NetconfSessionCapabilities, NetconfMessage> remoteDevice) {
+ final RemoteDevice<NetconfSessionCapabilities, NetconfMessage> remoteDevice) {
+ this(id, remoteDevice, Optional.<NetconfSessionCapabilities>absent());
+ }
+
+ private NetconfDeviceCommunicator(final RemoteDeviceId id, final RemoteDevice<NetconfSessionCapabilities, NetconfMessage> remoteDevice,
+ final Optional<NetconfSessionCapabilities> overrideNetconfCapabilities) {
this.id = id;
this.remoteDevice = remoteDevice;
+ this.overrideNetconfCapabilities = overrideNetconfCapabilities;
}
- private final Queue<Request> requests = new ArrayDeque<>();
- private NetconfClientSession session;
-
@Override
public void onSessionUp(final NetconfClientSession session) {
sessionLock.lock();
logger.debug("{}: Session established", id);
this.session = session;
- final NetconfSessionCapabilities netconfSessionCapabilities =
+ NetconfSessionCapabilities netconfSessionCapabilities =
NetconfSessionCapabilities.fromNetconfSession(session);
logger.trace("{}: Session advertised capabilities: {}", id, netconfSessionCapabilities);
+ if(overrideNetconfCapabilities.isPresent()) {
+ netconfSessionCapabilities = netconfSessionCapabilities.replaceModuleCaps(overrideNetconfCapabilities.get());
+ logger.debug("{}: Session capabilities overridden, capabilities that will be used: {}", id, netconfSessionCapabilities);
+ }
+
remoteDevice.onRemoteSessionUp(netconfSessionCapabilities, this);
}
finally {
return;
}
- request.future.set( RpcResultBuilder.<NetconfMessage>success( message ).build() );
+ request.future.set( RpcResultBuilder.success( message ).build() );
}
}
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.LoggerFactory;
public final class NetconfSessionCapabilities {
+
private static final class ParameterMatcher {
private final Predicate<String> predicate;
private final int skipLength;
};
private final Set<QName> moduleBasedCaps;
- private final Set<String> capabilities;
+ private final Set<String> nonModuleCaps;
- private NetconfSessionCapabilities(final Set<String> capabilities, final Set<QName> moduleBasedCaps) {
- this.capabilities = Preconditions.checkNotNull(capabilities);
+ private NetconfSessionCapabilities(final Set<String> nonModuleCaps, final Set<QName> moduleBasedCaps) {
+ this.nonModuleCaps = Preconditions.checkNotNull(nonModuleCaps);
this.moduleBasedCaps = Preconditions.checkNotNull(moduleBasedCaps);
}
return moduleBasedCaps;
}
- public boolean containsCapability(final String capability) {
- return capabilities.contains(capability);
+ public Set<String> getNonModuleCaps() {
+ return nonModuleCaps;
+ }
+
+ public boolean containsNonModuleCapability(final String capability) {
+ return nonModuleCaps.contains(capability);
}
- public boolean containsCapability(final QName capability) {
+ public boolean containsModuleCapability(final QName capability) {
return moduleBasedCaps.contains(capability);
}
@Override
public String toString() {
return Objects.toStringHelper(this)
- .add("capabilities", capabilities)
+ .add("capabilities", nonModuleCaps)
+ .add("moduleBasedCapabilities", moduleBasedCaps)
.add("rollback", isRollbackSupported())
.add("monitoring", isMonitoringSupported())
.toString();
}
public boolean isRollbackSupported() {
- return containsCapability(NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString());
+ return containsNonModuleCapability(NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString());
}
public boolean isMonitoringSupported() {
- return containsCapability(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING)
- || containsCapability(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING.getNamespace().toString());
+ return containsModuleCapability(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING)
+ || containsNonModuleCapability(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING.getNamespace().toString());
+ }
+
+ public NetconfSessionCapabilities replaceModuleCaps(final NetconfSessionCapabilities netconfSessionModuleCapabilities) {
+ final Set<QName> moduleBasedCaps = Sets.newHashSet(netconfSessionModuleCapabilities.getModuleBasedCaps());
+
+ // Preserve monitoring module, since it indicates support for ietf-netconf-monitoring
+ if(containsModuleCapability(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING)) {
+ moduleBasedCaps.add(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING);
+ }
+ return new NetconfSessionCapabilities(getNonModuleCaps(), moduleBasedCaps);
}
public static NetconfSessionCapabilities fromNetconfSession(final NetconfClientSession session) {
public static NetconfSessionCapabilities fromStrings(final Collection<String> capabilities) {
final Set<QName> moduleBasedCaps = new HashSet<>();
+ final Set<String> nonModuleCaps = Sets.newHashSet(capabilities);
for (final String capability : capabilities) {
final int qmark = capability.indexOf('?');
String revision = REVISION_PARAM.from(queryParams);
if (revision != null) {
moduleBasedCaps.add(QName.create(namespace, revision, moduleName));
+ nonModuleCaps.remove(capability);
continue;
}
// FIXME: do we really want to continue here?
moduleBasedCaps.add(QName.create(namespace, revision, moduleName));
+ nonModuleCaps.remove(capability);
}
- return new NetconfSessionCapabilities(ImmutableSet.copyOf(capabilities), ImmutableSet.copyOf(moduleBasedCaps));
+ return new NetconfSessionCapabilities(ImmutableSet.copyOf(nonModuleCaps), ImmutableSet.copyOf(moduleBasedCaps));
}
}
@Override
public DOMDataReadOnlyTransaction newReadOnlyTransaction() {
- return new NetconfDeviceReadOnlyTx(rpc, normalizer);
+ return new NetconfDeviceReadOnlyTx(rpc, normalizer, id);
}
@Override
import com.google.common.base.Function;
import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
import org.opendaylight.controller.md.sal.common.impl.util.compat.DataNormalizer;
import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction;
import org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil;
+import org.opendaylight.controller.sal.connect.util.RemoteDeviceId;
import org.opendaylight.controller.sal.core.api.RpcImplementation;
import org.opendaylight.yangtools.yang.common.RpcResult;
import org.opendaylight.yangtools.yang.data.api.CompositeNode;
private final RpcImplementation rpc;
private final DataNormalizer normalizer;
+ private final RemoteDeviceId id;
- public NetconfDeviceReadOnlyTx(final RpcImplementation rpc, final DataNormalizer normalizer) {
+ public NetconfDeviceReadOnlyTx(final RpcImplementation rpc, final DataNormalizer normalizer, final RemoteDeviceId id) {
this.rpc = rpc;
this.normalizer = normalizer;
+ this.id = id;
}
public ListenableFuture<Optional<NormalizedNode<?, ?>>> readConfigurationData(final YangInstanceIdentifier path) {
return Futures.transform(future, new Function<RpcResult<CompositeNode>, Optional<NormalizedNode<?, ?>>>() {
@Override
public Optional<NormalizedNode<?, ?>> apply(final RpcResult<CompositeNode> result) {
+ checkReadSuccess(result, path);
+
final CompositeNode data = result.getResult().getFirstCompositeByName(NETCONF_DATA_QNAME);
final CompositeNode node = (CompositeNode) findNode(data, path);
});
}
+ private void checkReadSuccess(final RpcResult<CompositeNode> result, final YangInstanceIdentifier path) {
+ LOG.warn("{}: Unable to read data: {}, errors: {}", id, path, result.getErrors());
+ Preconditions.checkArgument(result.isSuccessful(), "%s: Unable to read data: %s, errors: %s", id, path, result.getErrors());
+ }
+
private Optional<NormalizedNode<?, ?>> transform(final YangInstanceIdentifier path, final CompositeNode node) {
if(node == null) {
return Optional.absent();
try {
return Optional.<NormalizedNode<?, ?>>of(normalizer.toNormalized(path, node).getValue());
} catch (final Exception e) {
- LOG.error("Unable to normalize data for {}, data: {}", path, node, e);
+ LOG.error("{}: Unable to normalize data for {}, data: {}", id, path, node, e);
throw e;
}
}
return Futures.transform(future, new Function<RpcResult<CompositeNode>, Optional<NormalizedNode<?, ?>>>() {
@Override
public Optional<NormalizedNode<?, ?>> apply(final RpcResult<CompositeNode> result) {
+ checkReadSuccess(result, path);
+
final CompositeNode data = result.getResult().getFirstCompositeByName(NETCONF_DATA_QNAME);
final CompositeNode node = (CompositeNode) findNode(data, path);
@Override
public ListenableFuture<Optional<NormalizedNode<?, ?>>> read(final LogicalDatastoreType store, final YangInstanceIdentifier path) {
- final YangInstanceIdentifier legacyPath = toLegacyPath(normalizer, path);
+ final YangInstanceIdentifier legacyPath = toLegacyPath(normalizer, path, id);
switch (store) {
case CONFIGURATION : {
}
}
- throw new IllegalArgumentException(String.format("Cannot read data %s for %s datastore, unknown datastore type", path, store));
+ throw new IllegalArgumentException(String.format("%s, Cannot read data %s for %s datastore, unknown datastore type", id, path, store));
}
- static YangInstanceIdentifier toLegacyPath(final DataNormalizer normalizer, final YangInstanceIdentifier path) {
+ static YangInstanceIdentifier toLegacyPath(final DataNormalizer normalizer, final YangInstanceIdentifier path, final RemoteDeviceId id) {
try {
return normalizer.toLegacy(path);
} catch (final DataNormalizationException e) {
- throw new IllegalArgumentException("Cannot normalize path " + path, e);
+ throw new IllegalArgumentException(id + ": Cannot normalize path " + path, e);
}
}
@Override
public void put(final LogicalDatastoreType store, final YangInstanceIdentifier path, final NormalizedNode<?, ?> data) {
- Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can merge only configuration, not %s", store);
+ Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can replaceModuleCaps only configuration, not %s", store);
try {
- final YangInstanceIdentifier legacyPath = NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path);
+ final YangInstanceIdentifier legacyPath = NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path, id);
final CompositeNode legacyData = normalizer.toLegacy(path, data);
sendEditRpc(createEditConfigStructure(legacyPath, Optional.of(ModifyAction.REPLACE), Optional.fromNullable(legacyData)), Optional.of(ModifyAction.NONE));
} catch (final ExecutionException e) {
@Override
public void merge(final LogicalDatastoreType store, final YangInstanceIdentifier path, final NormalizedNode<?, ?> data) {
- Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can merge only configuration, not %s", store);
+ Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can replaceModuleCaps only configuration, not %s", store);
try {
- final YangInstanceIdentifier legacyPath = NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path);
+ final YangInstanceIdentifier legacyPath = NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path, id);
final CompositeNode legacyData = normalizer.toLegacy(path, data);
sendEditRpc(
createEditConfigStructure(legacyPath, Optional.<ModifyAction> absent(), Optional.fromNullable(legacyData)), Optional.<ModifyAction> absent());
@Override
public void delete(final LogicalDatastoreType store, final YangInstanceIdentifier path) {
- Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can merge only configuration, not %s", store);
+ Preconditions.checkArgument(store == LogicalDatastoreType.CONFIGURATION, "Can replaceModuleCaps only configuration, not %s", store);
try {
- sendEditRpc(createEditConfigStructure(NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path), Optional.of(ModifyAction.DELETE), Optional.<CompositeNode>absent()), Optional.of(ModifyAction.NONE));
+ sendEditRpc(createEditConfigStructure(NetconfDeviceReadOnlyTx.toLegacyPath(normalizer, path, id), Optional.of(ModifyAction.DELETE), Optional.<CompositeNode>absent()), Optional.of(ModifyAction.NONE));
} catch (final ExecutionException e) {
LOG.warn("Error deleting data {}, discarding changes", path, e);
discardChanges();
type string;
}
+ container yang-module-capabilities {
+ leaf-list capability {
+ type string;
+ description "Set a list of capabilities to override capabilities provided in device's hello message.
+ Can be used for devices that do not report any yang modules in their hello message";
+ }
+ }
+
container dom-registry {
uses config:service-ref {
refine type {
package org.opendaylight.controller.sal.connect.netconf.listener;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.opendaylight.controller.netconf.api.xml.XmlNetconfConstants.RPC_REPLY_KEY;
+import static org.opendaylight.controller.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ListenableFuture;
import io.netty.channel.ChannelFuture;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
-
import java.io.ByteArrayInputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
-
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.same;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.opendaylight.controller.netconf.api.xml.XmlNetconfConstants.RPC_REPLY_KEY;
-import static org.opendaylight.controller.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0;
-
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
-import com.google.common.base.Strings;
-import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.ListenableFuture;
-
public class NetconfDeviceCommunicatorTest {
@Mock
verify( mockDevice ).onRemoteSessionUp( netconfSessionCapabilities.capture(), eq( communicator ) );
NetconfSessionCapabilities actualCapabilites = netconfSessionCapabilities.getValue();
- assertEquals( "containsCapability", true, actualCapabilites.containsCapability(
- NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString() ) );
- assertEquals( "containsCapability", true, actualCapabilites.containsCapability( testCapability ) );
+ assertEquals( "containsModuleCapability", true, actualCapabilites.containsNonModuleCapability(
+ NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString()) );
+ assertEquals( "containsModuleCapability", false, actualCapabilites.containsNonModuleCapability(testCapability) );
assertEquals( "getModuleBasedCaps", Sets.newHashSet(
QName.create( "urn:opendaylight:params:xml:ns:test", "2014-06-02", "test-module" )),
actualCapabilites.getModuleBasedCaps() );
--- /dev/null
+package org.opendaylight.controller.sal.connect.netconf.listener;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import org.junit.Test;
+import org.junit.matchers.JUnitMatchers;
+import org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil;
+import org.opendaylight.yangtools.yang.common.QName;
+
+public class NetconfSessionCapabilitiesTest {
+
+ @Test
+ public void testMerge() throws Exception {
+ final List<String> caps1 = Lists.newArrayList(
+ "namespace:1?module=module1&revision=2012-12-12",
+ "namespace:2?module=module2&revision=2012-12-12",
+ "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring?module=ietf-netconf-monitoring&revision=2010-10-04",
+ "urn:ietf:params:netconf:base:1.0",
+ "urn:ietf:params:netconf:capability:rollback-on-error:1.0"
+ );
+ final NetconfSessionCapabilities sessionCaps1 = NetconfSessionCapabilities.fromStrings(caps1);
+ assertCaps(sessionCaps1, 2, 3);
+
+ final List<String> caps2 = Lists.newArrayList(
+ "namespace:3?module=module3&revision=2012-12-12",
+ "namespace:4?module=module4&revision=2012-12-12",
+ "randomNonModuleCap"
+ );
+ final NetconfSessionCapabilities sessionCaps2 = NetconfSessionCapabilities.fromStrings(caps2);
+ assertCaps(sessionCaps2, 1, 2);
+
+ final NetconfSessionCapabilities merged = sessionCaps1.replaceModuleCaps(sessionCaps2);
+ assertCaps(merged, 2, 2 + 1 /*Preserved monitoring*/);
+ for (final QName qName : sessionCaps2.getModuleBasedCaps()) {
+ assertThat(merged.getModuleBasedCaps(), JUnitMatchers.hasItem(qName));
+ }
+ assertThat(merged.getModuleBasedCaps(), JUnitMatchers.hasItem(NetconfMessageTransformUtil.IETF_NETCONF_MONITORING));
+
+ assertThat(merged.getNonModuleCaps(), JUnitMatchers.hasItem("urn:ietf:params:netconf:base:1.0"));
+ assertThat(merged.getNonModuleCaps(), JUnitMatchers.hasItem("urn:ietf:params:netconf:capability:rollback-on-error:1.0"));
+ }
+
+ private void assertCaps(final NetconfSessionCapabilities sessionCaps1, final int nonModuleCaps, final int moduleCaps) {
+ assertEquals(nonModuleCaps, sessionCaps1.getNonModuleCaps().size());
+ assertEquals(moduleCaps, sessionCaps1.getModuleBasedCaps().size());
+ }
+}