Merge "BUG-628 Allow configuration to override module based capabilities from remote...
authorTony Tkacik <ttkacik@cisco.com>
Fri, 1 Aug 2014 08:27:50 +0000 (08:27 +0000)
committerGerrit Code Review <gerrit@opendaylight.org>
Fri, 1 Aug 2014 08:27:50 +0000 (08:27 +0000)
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/config/yang/md/sal/connector/netconf/NetconfConnectorModule.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/NetconfDevice.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/listener/NetconfDeviceCommunicator.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/listener/NetconfSessionCapabilities.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/sal/NetconfDeviceDataBroker.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/sal/tx/NetconfDeviceReadOnlyTx.java
opendaylight/md-sal/sal-netconf-connector/src/main/java/org/opendaylight/controller/sal/connect/netconf/sal/tx/NetconfDeviceWriteOnlyTx.java
opendaylight/md-sal/sal-netconf-connector/src/main/yang/odl-sal-netconf-connector-cfg.yang
opendaylight/md-sal/sal-netconf-connector/src/test/java/org/opendaylight/controller/sal/connect/netconf/listener/NetconfDeviceCommunicatorTest.java
opendaylight/md-sal/sal-netconf-connector/src/test/java/org/opendaylight/controller/sal/connect/netconf/listener/NetconfSessionCapabilitiesTest.java [new file with mode: 0644]

index 037bfb4..b75df80 100644 (file)
@@ -13,8 +13,10 @@ import static org.opendaylight.controller.config.api.JmxAttributeValidationExcep
 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;
@@ -24,6 +26,7 @@ import org.opendaylight.controller.sal.binding.api.BindingAwareBroker;
 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;
@@ -40,6 +43,8 @@ import org.osgi.framework.BundleContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Optional;
+
 /**
  *
  */
@@ -49,6 +54,7 @@ public final class NetconfConnectorModule extends org.opendaylight.controller.co
 
     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);
@@ -82,9 +88,11 @@ public final class NetconfConnectorModule extends org.opendaylight.controller.co
             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);
     }
@@ -98,10 +106,14 @@ public final class NetconfConnectorModule extends org.opendaylight.controller.co
         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();
@@ -116,6 +128,26 @@ public final class NetconfConnectorModule extends org.opendaylight.controller.co
         };
     }
 
+    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";
@@ -175,8 +207,8 @@ public final class NetconfConnectorModule extends org.opendaylight.controller.co
         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());
         }
     }
index de4ac7a..07d3c08 100644 (file)
@@ -118,6 +118,7 @@ public final class NetconfDevice implements RemoteDevice<NetconfSessionCapabilit
                 // 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
             }
         });
     }
index 8045f8c..3871cdf 100644 (file)
@@ -7,13 +7,19 @@
  */
 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;
@@ -36,31 +42,35 @@ import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
 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();
@@ -68,10 +78,15 @@ public class NetconfDeviceCommunicator implements NetconfClientSessionListener,
             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 {
@@ -223,7 +238,7 @@ public class NetconfDeviceCommunicator implements NetconfClientSessionListener,
                 return;
             }
 
-            request.future.set( RpcResultBuilder.<NetconfMessage>success( message ).build() );
+            request.future.set( RpcResultBuilder.success( message ).build() );
         }
     }
 
index 8964a80..7800ae6 100644 (file)
@@ -8,6 +8,7 @@ import com.google.common.base.Splitter;
 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;
@@ -19,6 +20,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public final class NetconfSessionCapabilities {
+
     private static final class ParameterMatcher {
         private final Predicate<String> predicate;
         private final int skipLength;
@@ -57,10 +59,10 @@ public final class NetconfSessionCapabilities {
     };
 
     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);
     }
 
@@ -68,30 +70,45 @@ public final class NetconfSessionCapabilities {
         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) {
@@ -100,6 +117,7 @@ public final class NetconfSessionCapabilities {
 
     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('?');
@@ -117,6 +135,7 @@ public final class NetconfSessionCapabilities {
             String revision = REVISION_PARAM.from(queryParams);
             if (revision != null) {
                 moduleBasedCaps.add(QName.create(namespace, revision, moduleName));
+                nonModuleCaps.remove(capability);
                 continue;
             }
 
@@ -136,8 +155,9 @@ public final class NetconfSessionCapabilities {
 
             // 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));
     }
 }
index ee0c8b7..817fc29 100644 (file)
@@ -42,7 +42,7 @@ final class NetconfDeviceDataBroker implements DOMDataBroker {
 
     @Override
     public DOMDataReadOnlyTransaction newReadOnlyTransaction() {
-        return new NetconfDeviceReadOnlyTx(rpc, normalizer);
+        return new NetconfDeviceReadOnlyTx(rpc, normalizer, id);
     }
 
     @Override
index 3248453..03ee2d6 100644 (file)
@@ -15,6 +15,7 @@ import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessag
 
 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;
@@ -22,6 +23,7 @@ import org.opendaylight.controller.md.sal.common.impl.util.compat.DataNormalizat
 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;
@@ -38,10 +40,12 @@ public final class NetconfDeviceReadOnlyTx implements DOMDataReadOnlyTransaction
 
     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) {
@@ -51,6 +55,8 @@ public final class NetconfDeviceReadOnlyTx implements DOMDataReadOnlyTransaction
         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);
 
@@ -61,6 +67,11 @@ public final class NetconfDeviceReadOnlyTx implements DOMDataReadOnlyTransaction
         });
     }
 
+    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();
@@ -68,7 +79,7 @@ public final class NetconfDeviceReadOnlyTx implements DOMDataReadOnlyTransaction
         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;
         }
     }
@@ -79,6 +90,8 @@ public final class NetconfDeviceReadOnlyTx implements DOMDataReadOnlyTransaction
         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);
 
@@ -123,7 +136,7 @@ public final class NetconfDeviceReadOnlyTx implements DOMDataReadOnlyTransaction
 
     @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 : {
@@ -134,14 +147,14 @@ public final class NetconfDeviceReadOnlyTx implements DOMDataReadOnlyTransaction
             }
         }
 
-        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);
         }
     }
 
index c8d9028..6f6e814 100644 (file)
@@ -100,10 +100,10 @@ public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction {
 
     @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) {
@@ -115,10 +115,10 @@ public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction {
 
     @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());
@@ -131,10 +131,10 @@ public class NetconfDeviceWriteOnlyTx implements DOMDataWriteTransaction {
 
     @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();
index 6bad479..e13398b 100644 (file)
@@ -58,6 +58,14 @@ module odl-sal-netconf-connector-cfg {
                 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 {
index 391bf9c..001b9a8 100644 (file)
@@ -8,35 +8,35 @@
 
 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;
@@ -56,10 +56,6 @@ import org.opendaylight.yangtools.yang.common.RpcResult;
 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
@@ -129,9 +125,9 @@ public class NetconfDeviceCommunicatorTest {
         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() );
diff --git a/opendaylight/md-sal/sal-netconf-connector/src/test/java/org/opendaylight/controller/sal/connect/netconf/listener/NetconfSessionCapabilitiesTest.java b/opendaylight/md-sal/sal-netconf-connector/src/test/java/org/opendaylight/controller/sal/connect/netconf/listener/NetconfSessionCapabilitiesTest.java
new file mode 100644 (file)
index 0000000..87947b5
--- /dev/null
@@ -0,0 +1,50 @@
+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&amp;revision=2012-12-12",
+                "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring?module=ietf-netconf-monitoring&amp;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());
+    }
+}