*/
package org.opendaylight.controller.config.manager.impl.osgi;
+import org.opendaylight.controller.config.api.ConflictingVersionException;
import org.opendaylight.controller.config.api.jmx.CommitStatus;
import org.opendaylight.controller.config.manager.impl.ConfigRegistryImpl;
import org.opendaylight.controller.config.spi.ModuleFactory;
/**
* Every time factory is added or removed, blank transaction is triggered to handle
- * {@link org.opendaylight.controller.config.spi.ModuleFactory#getDefaultModules(org.opendaylight.controller.config.api.DependencyResolverFactory)}
+ * {@link org.opendaylight.controller.config.spi.ModuleFactory#getDefaultModules(org.opendaylight.controller.config.api.DependencyResolverFactory, org.osgi.framework.BundleContext)}
* functionality.
*/
public class BlankTransactionServiceTracker implements ServiceTrackerCustomizer<ModuleFactory, Object> {
}
private synchronized void blankTransaction() {
- // create transaction
- ObjectName tx = configRegistry.beginConfig();
- CommitStatus commitStatus = configRegistry.commitConfig(tx);
- logger.debug("Committed blank transaction with status {}", commitStatus);
+ // race condition check: config-persister might push new configuration while server is starting up.
+ ConflictingVersionException lastException = null;
+ for (int i = 0; i < 10; i++) {
+ try {
+ // create transaction
+ ObjectName tx = configRegistry.beginConfig();
+ CommitStatus commitStatus = configRegistry.commitConfig(tx);
+ logger.debug("Committed blank transaction with status {}", commitStatus);
+ return;
+ } catch (ConflictingVersionException e) {
+ lastException = e;
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException interruptedException) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(interruptedException);
+ }
+ }
+ }
+ throw lastException;
}
@Override
- public void modifiedService(ServiceReference<ModuleFactory> moduleFactoryServiceReference, Object o) {
+ public void modifiedService(ServiceReference <ModuleFactory> moduleFactoryServiceReference, Object o) {
blankTransaction();
}
package org.opendaylight.controller.sal.restconf.impl
-import javax.ws.rs.WebApplicationException
import javax.ws.rs.core.Response
import org.opendaylight.controller.md.sal.common.api.data.DataReader
import org.opendaylight.controller.sal.core.api.Broker.ConsumerSession
private String localName;
private URI namespace;
+ private QName name;
private List<NodeWrapper<?>> values = new ArrayList<>();
public CompositeNodeWrapper(String localName) {
this(localName);
this.namespace = namespace;
}
+
+ @Override
+ public void setQname(QName name) {
+ Preconditions.checkState(compositeNode == null, "Cannot change the object, due to data inconsistencies.");
+ this.name = name;
+ }
@Override
public String getLocalName() {
@Override
public CompositeNode unwrap(CompositeNode parent) {
if (compositeNode == null) {
- Preconditions.checkNotNull(namespace);
- compositeNode = NodeFactory.createMutableCompositeNode(new QName(namespace, localName),
- parent, new ArrayList<Node<?>>(), ModifyAction.CREATE, null);
+ if (name == null) {
+ Preconditions.checkNotNull(namespace);
+ name = new QName(namespace, localName);
+ }
+ compositeNode = NodeFactory.createMutableCompositeNode(name, parent, new ArrayList<Node<?>>(), null, null);
List<Node<?>> nodeValues = new ArrayList<>();
for (NodeWrapper<?> nodeWrapper : values) {
values = null;
namespace = null;
localName = null;
+ name = null;
}
return compositeNode;
}
private def dispatch CharSequence toRestconfIdentifier(PathArgument argument, DataSchemaNode node) {
throw new IllegalArgumentException("Conversion of generic path argument is not supported");
}
+
+ def findModuleByNamespace(URI namespace) {
+ checkPreconditions
+ var module = uriToModuleName.get(namespace)
+ if (module === null) {
+ val moduleSchemas = schemas.findModuleByNamespace(namespace);
+ if(moduleSchemas === null) throw new IllegalArgumentException()
+ var latestModule = moduleSchemas.head
+ for (m : moduleSchemas) {
+ if (m.revision.after(latestModule.revision)) {
+ latestModule = m
+ }
+ }
+ if(latestModule === null) throw new IllegalArgumentException()
+ uriToModuleName.put(namespace, latestModule.name)
+ module = latestModule.name;
+ }
+ return module
+ }
def CharSequence toRestconfIdentifier(QName qname) {
checkPreconditions
import java.net.URI;
+import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.CompositeNode;
import org.opendaylight.yangtools.yang.data.api.Node;
public interface NodeWrapper<T extends Node<?>> {
+ void setQname(QName name);
+
T unwrap(CompositeNode parent);
URI getNamespace();
package org.opendaylight.controller.sal.restconf.impl
import java.util.List
+import java.util.Set
import javax.ws.rs.core.Response
import org.opendaylight.controller.md.sal.common.api.TransactionStatus
import org.opendaylight.controller.sal.rest.api.RestconfService
import org.opendaylight.yangtools.yang.data.api.CompositeNode
+import org.opendaylight.yangtools.yang.model.api.ChoiceNode
import org.opendaylight.yangtools.yang.model.api.DataNodeContainer
import org.opendaylight.yangtools.yang.model.api.DataSchemaNode
override readConfigurationData(String identifier) {
val instanceIdentifierWithSchemaNode = identifier.resolveInstanceIdentifier
- val data = broker.readOperationalData(instanceIdentifierWithSchemaNode.getInstanceIdentifier);
+ val data = broker.readConfigurationData(instanceIdentifierWithSchemaNode.getInstanceIdentifier);
return new StructuredData(data, instanceIdentifierWithSchemaNode.schemaNode)
}
}
private def void addNamespaceToNodeFromSchemaRecursively(NodeWrapper<?> nodeBuilder, DataSchemaNode schema) {
- if (nodeBuilder.namespace === null) {
- nodeBuilder.namespace = schema.QName.namespace
+ if (schema === null) {
+ throw new ResponseException(Response.Status.BAD_REQUEST,
+ "Data has bad format\n" + nodeBuilder.localName + " does not exist in yang schema.");
+ }
+ val moduleName = controllerContext.findModuleByNamespace(schema.QName.namespace);
+ if (nodeBuilder.namespace === null || nodeBuilder.namespace == schema.QName.namespace ||
+ nodeBuilder.namespace.path == moduleName) {
+ nodeBuilder.qname = schema.QName
+ } else {
+ throw new ResponseException(Response.Status.BAD_REQUEST,
+ "Data has bad format\nIf data is in XML format then namespace for " + nodeBuilder.localName +
+ " should be " + schema.QName.namespace + ".\n If data is in Json format then module name for " +
+ nodeBuilder.localName + " should be " + moduleName + ".");
}
if (nodeBuilder instanceof CompositeNodeWrapper) {
val List<NodeWrapper<?>> children = (nodeBuilder as CompositeNodeWrapper).getValues
for (child : children) {
addNamespaceToNodeFromSchemaRecursively(child,
- (schema as DataNodeContainer).childNodes.findFirst[n|n.QName.localName.equals(child.localName)])
+ findFirstSchemaByLocalName(child.localName, (schema as DataNodeContainer).childNodes))
+ }
+ }
+ }
+
+ private def DataSchemaNode findFirstSchemaByLocalName(String localName, Set<DataSchemaNode> schemas) {
+ for (schema : schemas) {
+ if (schema instanceof ChoiceNode) {
+ for (caze : (schema as ChoiceNode).cases) {
+ val result = findFirstSchemaByLocalName(localName, caze.childNodes)
+ if (result !== null) {
+ return result
+ }
+ }
+ } else {
+ return schemas.findFirst[n|n.QName.localName.equals(localName)]
}
}
+ return null
}
}
private String localName;
private String value;
private URI namespace;
+ private QName name;
public SimpleNodeWrapper(String localName, String value) {
this.localName = Preconditions.checkNotNull(localName);
this.namespace = namespace;
}
+ @Override
+ public void setQname(QName name) {
+ Preconditions.checkState(simpleNode == null, "Cannot change the object, due to data inconsistencies.");
+ this.name = name;
+ }
+
@Override
public String getLocalName() {
if (simpleNode != null) {
@Override
public SimpleNode<String> unwrap(CompositeNode parent) {
if (simpleNode == null) {
- Preconditions.checkNotNull(namespace);
- simpleNode = NodeFactory.createImmutableSimpleNode(new QName(namespace, localName), parent, value);
+ if (name == null) {
+ Preconditions.checkNotNull(namespace);
+ name = new QName(namespace, localName);
+ }
+ simpleNode = NodeFactory.createImmutableSimpleNode(name, parent, value);
value = null;
namespace = null;
localName = null;
+ name = null;
}
return simpleNode;
}
org.slf4j,
org.w3c.dom,
org.xml.sax,
+ javax.xml.namespace,
+ javax.xml.xpath,
+ org.opendaylight.controller.config.api
</Import-Package>
<Export-Package>
</Export-Package>
import com.google.common.collect.Sets;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
+import org.opendaylight.controller.config.api.ConflictingVersionException;
import org.opendaylight.controller.config.persist.api.ConfigSnapshotHolder;
import org.opendaylight.controller.config.persist.api.Persister;
import org.opendaylight.controller.netconf.api.NetconfMessage;
import org.opendaylight.controller.netconf.api.jmx.NetconfJMXNotification;
import org.opendaylight.controller.netconf.client.NetconfClient;
import org.opendaylight.controller.netconf.client.NetconfClientDispatcher;
+import org.opendaylight.controller.netconf.util.xml.XMLNetconfUtil;
import org.opendaylight.controller.netconf.util.xml.XmlElement;
import org.opendaylight.controller.netconf.util.xml.XmlNetconfConstants;
import org.opendaylight.controller.netconf.util.xml.XmlUtil;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.net.ssl.SSLContext;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
private final Persister persister;
private final MBeanServerConnection mbeanServer;
- private Long currentSessionId;
+
private final ObjectName on = DefaultCommitOperationMXBean.objectName;
if (maybeConfig.isPresent()) {
logger.debug("Last config found {}", persister);
-
- registerToNetconf(maybeConfig.get().getCapabilities());
-
- final String configSnapshot = maybeConfig.get().getConfigSnapshot();
- logger.trace("Pushing following xml to netconf {}", configSnapshot);
- try {
- pushLastConfig(XmlUtil.readXmlToElement(configSnapshot));
- } catch (SAXException | IOException e) {
- throw new IllegalStateException("Unable to load last config", e);
+ ConflictingVersionException lastException = null;
+ int maxAttempts = 30;
+ for(int i = 0 ; i < maxAttempts; i++) {
+ registerToNetconf(maybeConfig.get().getCapabilities());
+
+ final String configSnapshot = maybeConfig.get().getConfigSnapshot();
+ logger.trace("Pushing following xml to netconf {}", configSnapshot);
+ try {
+ pushLastConfig(XmlUtil.readXmlToElement(configSnapshot));
+ } catch(ConflictingVersionException e) {
+ closeClientAndDispatcher(netconfClient, netconfClientDispatcher);
+ lastException = e;
+ Thread.sleep(1000);
+ } catch (SAXException | IOException e) {
+ throw new IllegalStateException("Unable to load last config", e);
+ }
}
+ throw new IllegalStateException("Failed to push configuration, maximum attempt count has been reached: "
+ + maxAttempts, lastException);
} else {
// this ensures that netconf is initialized, this is first
int attempt = 0;
- while (true) {
+ long deadline = pollingStart + timeout;
+ while (System.currentTimeMillis() < deadline) {
attempt++;
-
netconfClientDispatcher = new NetconfClientDispatcher(Optional.<SSLContext>absent(), nettyThreadgroup, nettyThreadgroup);
try {
netconfClient = new NetconfClient(this.toString(), address, delay, netconfClientDispatcher);
if (isSubset(currentCapabilities, expectedCaps)) {
logger.debug("Hello from netconf stable with {} capabilities", currentCapabilities);
- currentSessionId = netconfClient.getSessionId();
+ long currentSessionId = netconfClient.getSessionId();
logger.info("Session id received from netconf server: {}", currentSessionId);
return currentSessionId;
}
- if (System.currentTimeMillis() > pollingStart + timeout) {
- break;
- }
+
logger.debug("Polling hello from netconf, attempt {}, capabilities {}", attempt, currentCapabilities);
return maybeConfigElement;
}
- private synchronized void pushLastConfig(Element xmlToBePersisted) {
+ private synchronized void pushLastConfig(Element xmlToBePersisted) throws ConflictingVersionException {
logger.info("Pushing last configuration to netconf");
StringBuilder response = new StringBuilder("editConfig response = {");
logger.trace("Detailed message {}", response);
}
- private void checkIsOk(XmlElement element, NetconfMessage responseMessage) {
+ static void checkIsOk(XmlElement element, NetconfMessage responseMessage) throws ConflictingVersionException {
if (element.getName().equals(XmlNetconfConstants.OK)) {
return;
- } else {
- if (element.getName().equals(XmlNetconfConstants.RPC_ERROR)) {
- logger.warn("Can not load last configuration, operation failed");
- throw new IllegalStateException("Can not load last configuration, operation failed: "
- + XmlUtil.toString(responseMessage.getDocument()));
+ }
+
+ if (element.getName().equals(XmlNetconfConstants.RPC_ERROR)) {
+ logger.warn("Can not load last configuration, operation failed");
+ // is it ConflictingVersionException ?
+ XPathExpression xPathExpression = XMLNetconfUtil.compileXPath("/netconf:rpc-reply/netconf:rpc-error/netconf:error-info/netconf:error");
+ String error = (String) XmlUtil.evaluateXPath(xPathExpression, element.getDomElement(), XPathConstants.STRING);
+ if (error!=null && error.contains(ConflictingVersionException.class.getCanonicalName())) {
+ throw new ConflictingVersionException(error);
}
- logger.warn("Can not load last configuration. Operation failed.");
- throw new IllegalStateException("Can not load last configuration. Operation failed: "
+ throw new IllegalStateException("Can not load last configuration, operation failed: "
+ XmlUtil.toString(responseMessage.getDocument()));
}
+
+ logger.warn("Can not load last configuration. Operation failed.");
+ throw new IllegalStateException("Can not load last configuration. Operation failed: "
+ + XmlUtil.toString(responseMessage.getDocument()));
}
private static NetconfMessage createEditConfigMessage(Element dataElement, String editConfigResourcename) {
--- /dev/null
+/*
+ * Copyright (c) 2013 Cisco Systems, Inc. and others. 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.controller.netconf.persist.impl;
+
+import org.junit.Test;
+import org.opendaylight.controller.config.api.ConflictingVersionException;
+import org.opendaylight.controller.netconf.api.NetconfMessage;
+import org.opendaylight.controller.netconf.util.xml.XmlElement;
+import org.opendaylight.controller.netconf.util.xml.XmlUtil;
+import org.w3c.dom.Document;
+
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.junit.matchers.JUnitMatchers.containsString;
+
+public class ConfigPersisterNotificationHandlerTest {
+
+ @Test
+ public void testConflictingVersionDetection() throws Exception {
+ Document document = XmlUtil.readXmlToDocument(getClass().getResourceAsStream("/conflictingVersionResponse.xml"));
+ try{
+ ConfigPersisterNotificationHandler.checkIsOk(XmlElement.fromDomDocument(document).getOnlyChildElement(), new NetconfMessage(document));
+ fail();
+ }catch(ConflictingVersionException e){
+ assertThat(e.getMessage(), containsString("Optimistic lock failed. Expected parent version 21, was 18"));
+ }
+ }
+
+}
--- /dev/null
+<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="persister_commit">
+ <rpc-error>
+ <error-type>application</error-type>
+ <error-tag>operation-failed</error-tag>
+ <error-severity>error</error-severity>
+
+
+
+ <error-info>
+ <error>org.opendaylight.controller.config.api.ConflictingVersionException: Optimistic lock failed. Expected parent version 21, was 18</error>
+ </error-info>
+ </rpc-error>
+</rpc-reply>