edit-config, and then unlocks it.
This poses some scalabilty issues when multiple requests for
the same device arrives. Also some devices automatically
queues the requests at the device itself.
The patch provides an option to allow lock/unlock operations
for edit-config. Default value is true which defaults to today's
behavior.
JIRA: NETCONF-629
Change-Id: I0531b27c0807dfb586f9e80c9b9fc9b189e3cadf
Signed-off-by: Jakub Toth <jtoth@luminanetworks.com>
@Override
protected RemoteDeviceHandler<NetconfSessionPreferences> createSalFacade(final RemoteDeviceId id) {
- return new NetconfDeviceSalFacade(id, mountPointService, dataBroker);
+ return new NetconfDeviceSalFacade(id, mountPointService, dataBroker, topologyId);
}
}
@Override
protected RemoteDeviceHandler<NetconfSessionPreferences> createSalFacade(final RemoteDeviceId id) {
- return new NetconfDeviceSalFacade(id, mountPointService, dataBroker);
+ return new NetconfDeviceSalFacade(id, mountPointService, dataBroker, topologyId);
}
/**
--- /dev/null
+/*
+ * Copyright (c) 2019 Lumina Networks, Inc. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.sal.connect.netconf.sal;
+
+import java.util.Collection;
+import org.opendaylight.mdsal.binding.api.DataObjectModification;
+import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
+import org.opendaylight.mdsal.binding.api.DataTreeModification;
+import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev190614.netconf.node.fields.optional.topology.node.DatastoreLock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class LockChangeListener implements DataTreeChangeListener<DatastoreLock> {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LockChangeListener.class);
+
+ private final NetconfDeviceDataBroker netconfDeviceDataBroker;
+
+ LockChangeListener(final DOMDataBroker netconfDeviceDataBrokder) {
+ this.netconfDeviceDataBroker = (NetconfDeviceDataBroker)netconfDeviceDataBrokder;
+ }
+
+ @Override
+ public void onDataTreeChanged(final Collection<DataTreeModification<DatastoreLock>> changes) {
+ for (final DataTreeModification<DatastoreLock> change : changes) {
+ final DataObjectModification<DatastoreLock> rootNode = change.getRootNode();
+ switch (rootNode.getModificationType()) {
+ case SUBTREE_MODIFIED:
+ case WRITE:
+ if (!rootNode.getDataAfter().isDatastoreLockAllowed()) {
+ LOG.warn("With blocking the lock/unlock operations, the user is coming to"
+ + "operate in a manner which is not supported. It must not exist"
+ + "any concurrent access to the data store - it may interfere with"
+ + "data consistency.");
+ }
+ netconfDeviceDataBroker.setLockAllowed(rootNode.getDataAfter().isDatastoreLockAllowed());
+ break;
+ case DELETE:
+ netconfDeviceDataBroker.setLockAllowed(true);
+ break;
+ default:
+ LOG.debug("Unsupported modification type: {}.", rootNode.getModificationType());
+ }
+ }
+ }
+}
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
public final class NetconfDeviceDataBroker implements PingPongMergingDOMDataBroker {
+
private final RemoteDeviceId id;
private final NetconfBaseOps netconfOps;
-
private final boolean rollbackSupport;
private final boolean candidateSupported;
private final boolean runningWritable;
+ private boolean isLockAllowed = true;
+
public NetconfDeviceDataBroker(final RemoteDeviceId id, final SchemaContext schemaContext,
final DOMRpcService rpc, final NetconfSessionPreferences netconfSessionPreferences) {
this.id = id;
public DOMDataTreeWriteTransaction newWriteOnlyTransaction() {
if (candidateSupported) {
if (runningWritable) {
- return new WriteCandidateRunningTx(id, netconfOps, rollbackSupport);
+ return new WriteCandidateRunningTx(id, netconfOps, rollbackSupport, isLockAllowed);
} else {
- return new WriteCandidateTx(id, netconfOps, rollbackSupport);
+ return new WriteCandidateTx(id, netconfOps, rollbackSupport, isLockAllowed);
}
} else {
- return new WriteRunningTx(id, netconfOps, rollbackSupport);
+ return new WriteRunningTx(id, netconfOps, rollbackSupport, isLockAllowed);
}
}
public ClassToInstanceMap<DOMDataBrokerExtension> getExtensions() {
return ImmutableClassToInstanceMap.of();
}
+
+ void setLockAllowed(boolean isLockAllowedOrig) {
+ this.isLockAllowed = isLockAllowedOrig;
+ }
+
}
import java.util.ArrayList;
import java.util.List;
import org.opendaylight.mdsal.binding.api.DataBroker;
+import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.dom.api.DOMActionService;
-import org.opendaylight.mdsal.dom.api.DOMDataBroker;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.mdsal.dom.api.DOMNotification;
import org.opendaylight.mdsal.dom.api.DOMRpcService;
import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfDeviceCapabilities;
import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfSessionPreferences;
import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev190614.NetconfNodeFieldsOptional;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev190614.netconf.node.fields.optional.Topology;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev190614.netconf.node.fields.optional.TopologyKey;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev190614.netconf.node.fields.optional.topology.Node;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev190614.netconf.node.fields.optional.topology.NodeKey;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.optional.rev190614.netconf.node.fields.optional.topology.node.DatastoreLock;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TopologyId;
+import org.opendaylight.yangtools.concepts.ListenerRegistration;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private final RemoteDeviceId id;
private final NetconfDeviceSalProvider salProvider;
private final List<AutoCloseable> salRegistrations = new ArrayList<>();
+ private final DataBroker dataBroker;
+ private final String topologyId;
+
+ private ListenerRegistration<LockChangeListener> listenerRegistration = null;
public NetconfDeviceSalFacade(final RemoteDeviceId id, final DOMMountPointService mountPointService,
- final DataBroker dataBroker) {
- this.id = id;
- this.salProvider = new NetconfDeviceSalProvider(id, mountPointService, dataBroker);
+ final DataBroker dataBroker, final String topologyId) {
+ this(id, new NetconfDeviceSalProvider(id, mountPointService, dataBroker), dataBroker, topologyId);
}
@VisibleForTesting
- NetconfDeviceSalFacade(final RemoteDeviceId id, final NetconfDeviceSalProvider salProvider) {
+ NetconfDeviceSalFacade(final RemoteDeviceId id, final NetconfDeviceSalProvider salProvider,
+ final DataBroker dataBroker, final String topologyId) {
this.id = id;
this.salProvider = salProvider;
+ this.dataBroker = dataBroker;
+ this.topologyId = topologyId;
}
@Override
final NetconfSessionPreferences netconfSessionPreferences,
final DOMRpcService deviceRpc, final DOMActionService deviceAction) {
- final DOMDataBroker domBroker =
+ final NetconfDeviceDataBroker netconfDeviceDataBroker =
new NetconfDeviceDataBroker(id, schemaContext, deviceRpc, netconfSessionPreferences);
-
+ registerLockListener(netconfDeviceDataBroker);
final NetconfDeviceNotificationService notificationService = new NetconfDeviceNotificationService();
salProvider.getMountInstance()
- .onTopologyDeviceConnected(schemaContext, domBroker, deviceRpc, notificationService, deviceAction);
+ .onTopologyDeviceConnected(schemaContext, netconfDeviceDataBroker, deviceRpc, notificationService,
+ deviceAction);
salProvider.getTopologyDatastoreAdapter()
.updateDeviceData(true, netconfSessionPreferences.getNetconfDeviceCapabilities());
}
public synchronized void onDeviceDisconnected() {
salProvider.getTopologyDatastoreAdapter().updateDeviceData(false, new NetconfDeviceCapabilities());
salProvider.getMountInstance().onTopologyDeviceDisconnected();
+ closeLockChangeListener();
}
@Override
public synchronized void onDeviceFailed(final Throwable throwable) {
salProvider.getTopologyDatastoreAdapter().setDeviceAsFailed(throwable);
salProvider.getMountInstance().onTopologyDeviceDisconnected();
+ closeLockChangeListener();
}
@Override
closeGracefully(reg);
}
closeGracefully(salProvider);
+ closeLockChangeListener();
}
@SuppressWarnings("checkstyle:IllegalCatch")
}
}
+ private void closeLockChangeListener() {
+ if (listenerRegistration != null) {
+ listenerRegistration.close();
+ }
+ }
+
+ private void registerLockListener(final NetconfDeviceDataBroker netconfDeviceDataBroker) {
+ listenerRegistration = dataBroker.registerDataTreeChangeListener(
+ DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, createTopologyListPath()),
+ new LockChangeListener(netconfDeviceDataBroker));
+ }
+
+ private InstanceIdentifier<DatastoreLock> createTopologyListPath() {
+ return InstanceIdentifier.create(NetconfNodeFieldsOptional.class)
+ .child(Topology.class, new TopologyKey(new TopologyId(topologyId)))
+ .child(Node.class, new NodeKey(new NodeId(id.getName())))
+ .child(DatastoreLock.class);
+
+ }
}
private final List<TxListener> listeners = new CopyOnWriteArrayList<>();
// Allow commit to be called only once
protected volatile boolean finished = false;
+ protected final boolean isLockAllowed;
- public AbstractWriteTx(final NetconfBaseOps netOps, final RemoteDeviceId id, final boolean rollbackSupport) {
- this.netOps = netOps;
+ public AbstractWriteTx(final RemoteDeviceId id, final NetconfBaseOps netconfOps, final boolean rollbackSupport,
+ final boolean isLockAllowed) {
+ this.netOps = netconfOps;
this.id = id;
this.rollbackSupport = rollbackSupport;
+ this.isLockAllowed = isLockAllowed;
init();
}
public WriteCandidateRunningTx(final RemoteDeviceId id, final NetconfBaseOps netOps,
final boolean rollbackSupport) {
- super(id, netOps, rollbackSupport);
+ this(id, netOps, rollbackSupport, true);
+ }
+
+ public WriteCandidateRunningTx(RemoteDeviceId id, NetconfBaseOps netconfOps, boolean rollbackSupport,
+ boolean isLockAllowed) {
+ super(id, netconfOps, rollbackSupport, isLockAllowed);
}
@Override
}
private void lockRunning() {
- resultsFutures.add(netOps.lockRunning(new NetconfRpcFutureCallback("Lock running", id)));
+ if (isLockAllowed) {
+ resultsFutures.add(netOps.lockRunning(new NetconfRpcFutureCallback("Lock running", id)));
+ } else {
+ LOG.trace("Lock is not allowed: {}", id);
+ }
}
/**
* and its netty threadpool that is really sensitive to blocking calls.
*/
private void unlockRunning() {
- netOps.unlockRunning(new NetconfRpcFutureCallback("Unlock running", id));
+ if (isLockAllowed) {
+ netOps.unlockRunning(new NetconfRpcFutureCallback("Unlock running", id));
+ } else {
+ LOG.trace("Unlock is not allowed: {}", id);
+ }
}
}
private static final Logger LOG = LoggerFactory.getLogger(WriteCandidateTx.class);
- public WriteCandidateTx(final RemoteDeviceId id, final NetconfBaseOps rpc, final boolean rollbackSupport) {
- super(rpc, id, rollbackSupport);
+ public WriteCandidateTx(final RemoteDeviceId id, final NetconfBaseOps netconfOps, final boolean rollbackSupport) {
+ this(id, netconfOps, rollbackSupport, true);
+ }
+
+ public WriteCandidateTx(RemoteDeviceId id, NetconfBaseOps netconfOps, boolean rollbackSupport,
+ boolean isLockAllowed) {
+ super(id, netconfOps, rollbackSupport, isLockAllowed);
}
@Override
}
private void lock() {
+ if (!isLockAllowed) {
+ LOG.trace("Lock is not allowed.");
+ return;
+ }
final FutureCallback<DOMRpcResult> lockCandidateCallback = new FutureCallback<DOMRpcResult>() {
@Override
public void onSuccess(final DOMRpcResult result) {
* and its netty threadpool that is really sensitive to blocking calls.
*/
private void unlock() {
- netOps.unlockCandidate(new NetconfRpcFutureCallback("Unlock candidate", id));
+ if (isLockAllowed) {
+ netOps.unlockCandidate(new NetconfRpcFutureCallback("Unlock candidate", id));
+ } else {
+ LOG.trace("Unlock is not allowed: {}", id);
+ }
}
}
public WriteRunningTx(final RemoteDeviceId id, final NetconfBaseOps netOps,
final boolean rollbackSupport) {
- super(netOps, id, rollbackSupport);
+ this(id, netOps, rollbackSupport, true);
+ }
+
+ public WriteRunningTx(RemoteDeviceId id, NetconfBaseOps netconfOps, boolean rollbackSupport,
+ boolean isLockAllowed) {
+ super(id, netconfOps, rollbackSupport, isLockAllowed);
}
@Override
}
private void lock() {
- resultsFutures.add(netOps.lockRunning(new NetconfRpcFutureCallback("Lock running", id)));
+ if (isLockAllowed) {
+ resultsFutures.add(netOps.lockRunning(new NetconfRpcFutureCallback("Lock running", id)));
+ } else {
+ LOG.trace("Lock is not allowed: {}", id);
+ }
}
@Override
}
private void unlock() {
- netOps.unlockRunning(new NetconfRpcFutureCallback("Unlock running", id));
+ if (isLockAllowed) {
+ netOps.unlockRunning(new NetconfRpcFutureCallback("Unlock running", id));
+ } else {
+ LOG.trace("Unlock is not allowed: {}", id);
+ }
}
private static final class Change {
--- /dev/null
+module netconf-node-optional {
+ namespace "urn:opendaylight:netconf-node-optional";
+ prefix "netnopt";
+
+ import network-topology { prefix nt; revision-date 2013-10-21; }
+
+ revision "2019-06-14" {
+ description "Initial revision of Node Locking model";
+ }
+
+ container netconf-node-fields-optional {
+ description "Allows to create node's optional value with the path mapping according to
+ the network-topology -> topology -> node";
+ list topology {
+ key topology-id;
+ leaf topology-id {
+ type nt:topology-id;
+ description "The name of node's topology";
+ }
+ list node {
+ key node-id;
+ leaf node-id {
+ type nt:node-id;
+ description "The identifier of a node in the topology";
+ }
+ // Containers allow to create specific data-change-listener directly on a node's optional value.
+ // In the future, it'll be easy to extend the node by optional node fields in this way. Do not create
+ // direct leafs here, please.
+ container datastore-lock {
+ description "Allows to ignore lock/unlock node's datastare.";
+ leaf datastore-lock-allowed {
+ type boolean;
+ default true;
+ description "The operation allows the client to lock the entire configuration datastore
+ system of a device.
+ WARNING - With blocking the lock/unlock operations, the user is coming to operate
+ in a manner which is not supported. It must not exist any concurrent access to
+ the data store - it may interfere with data consistency.";
+ }
+ }
+ }
+ }
+ }
+}
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
+import org.opendaylight.mdsal.binding.api.DataBroker;
import org.opendaylight.mdsal.dom.api.DOMDataBroker;
import org.opendaylight.mdsal.dom.api.DOMNotification;
import org.opendaylight.mdsal.dom.api.DOMRpcService;
private NetconfDeviceTopologyAdapter netconfDeviceTopologyAdapter;
@Mock
private NetconfDeviceSalProvider.MountInstance mountInstance;
-
@Mock
private NetconfDeviceSalProvider salProvider;
+ @Mock
+ private DataBroker dataBroker;
@Before
public void setUp() throws Exception {
final InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8000);
final RemoteDeviceId remoteDeviceId = new RemoteDeviceId("test", address);
- deviceFacade = new NetconfDeviceSalFacade(remoteDeviceId, salProvider);
+ deviceFacade = new NetconfDeviceSalFacade(remoteDeviceId, salProvider, dataBroker, "mockTopo");
doReturn(netconfDeviceTopologyAdapter).when(salProvider).getTopologyDatastoreAdapter();
doNothing().when(netconfDeviceTopologyAdapter)