--- /dev/null
+/*
+ * Copyright (c) 2018 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.netconf.mdsal.connector;
+
+import com.google.common.annotations.Beta;
+import com.google.common.util.concurrent.CheckedFuture;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataBrokerExtension;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction;
+import org.opendaylight.controller.md.sal.dom.api.DOMServiceExtension;
+import org.opendaylight.yangtools.yang.common.OperationFailedException;
+import org.opendaylight.yangtools.yang.common.RpcError;
+import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
+
+/**
+ * A {@link DOMServiceExtension} which allows users to provide Validate capability for {@link DOMDataBroker}.
+ *
+ * <p> See <a href="https://tools.ietf.org/html/rfc4741#section-8.6">RFC4741 section 8.6</a> for details.
+ */
+@Beta
+public interface DOMDataTransactionValidator extends DOMDataBrokerExtension {
+ /**
+ * Validates state of the data tree associated with the provided {@link DOMDataWriteTransaction}.
+ *
+ * <p>The operation should not have any side-effects on the transaction state.
+ *
+ * <p>It can be executed many times, providing the same results if the state of the transaction has not been
+ * changed.
+ *
+ * @param transaction
+ * transaction to be validated
+ * @return
+ * a CheckedFuture containing the result of the validate operation. The future blocks until the validation
+ * operation is complete. A successful validate returns nothing. On failure, the Future will fail
+ * with a {@link ValidationFailedException} or an exception derived from ValidationFailedException.
+ */
+ CheckedFuture<Void, ValidationFailedException> validate(DOMDataWriteTransaction transaction);
+
+ /**
+ * Failed validation of asynchronous transaction. This exception is raised and returned when transaction validation
+ * failed.
+ */
+ class ValidationFailedException extends OperationFailedException {
+ private static final long serialVersionUID = 1L;
+
+ public ValidationFailedException(final String message, final Throwable cause) {
+ super(message, cause, RpcResultBuilder.newError(RpcError.ErrorType.APPLICATION, "invalid-value", message,
+ null, null, cause));
+ }
+
+ public ValidationFailedException(final String message) {
+ this(message, null);
+ }
+ }
+}
+
import org.opendaylight.netconf.mdsal.connector.ops.Lock;
import org.opendaylight.netconf.mdsal.connector.ops.RuntimeRpc;
import org.opendaylight.netconf.mdsal.connector.ops.Unlock;
+import org.opendaylight.netconf.mdsal.connector.ops.Validate;
import org.opendaylight.netconf.mdsal.connector.ops.get.Get;
import org.opendaylight.netconf.mdsal.connector.ops.get.GetConfig;
new GetConfig(netconfSessionIdForReporting, schemaContext, transactionProvider),
new Lock(netconfSessionIdForReporting),
new Unlock(netconfSessionIdForReporting),
- new RuntimeRpc(netconfSessionIdForReporting, schemaContext, rpcService));
+ new RuntimeRpc(netconfSessionIdForReporting, schemaContext, rpcService),
+ new Validate(netconfSessionIdForReporting, transactionProvider));
}
Set<NetconfOperation> getOperations() {
package org.opendaylight.netconf.mdsal.connector;
+import static org.opendaylight.netconf.mdsal.connector.DOMDataTransactionValidator.ValidationFailedException;
+
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.CheckedFuture;
private DOMDataReadWriteTransaction candidateTransaction = null;
private DOMDataReadWriteTransaction runningTransaction = null;
private final List<DOMDataReadWriteTransaction> allOpenReadWriteTransactions = new ArrayList<>();
+ private final DOMDataTransactionValidator transactionValidator;
private final String netconfSessionIdForReporting;
public TransactionProvider(final DOMDataBroker dataBroker, final String netconfSessionIdForReporting) {
this.dataBroker = dataBroker;
this.netconfSessionIdForReporting = netconfSessionIdForReporting;
+ this.transactionValidator = (DOMDataTransactionValidator)dataBroker.getSupportedExtensions()
+ .get(DOMDataTransactionValidator.class);
}
@Override
return candidateTransaction;
}
+ public synchronized void validateTransaction() throws DocumentedException {
+ if (transactionValidator == null) {
+ LOG.error("Validate capability is not supported");
+ throw new DocumentedException("Validate capability is not supported",
+ ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED, ErrorSeverity.ERROR);
+ }
+
+ if (!getCandidateTransaction().isPresent()) {
+ // Validating empty transaction, just return true
+ LOG.debug("Validating empty candidate transaction for session {}", netconfSessionIdForReporting);
+ return;
+ }
+
+ try {
+ transactionValidator.validate(candidateTransaction).checkedGet();
+ } catch (final ValidationFailedException e) {
+ LOG.debug("Candidate transaction validation {} failed on session {}", candidateTransaction,
+ netconfSessionIdForReporting, e);
+ final String cause = e.getCause() != null ? (" Cause: " + e.getCause().getMessage()) : "";
+ throw new DocumentedException(
+ "Candidate transaction validate failed on " + e.getMessage() + " " + netconfSessionIdForReporting
+ + cause, e, ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
+ }
+ return;
+ }
+
public synchronized boolean commitTransaction() throws DocumentedException {
if (!getCandidateTransaction().isPresent()) {
//making empty commit without prior opened transaction, just return true
--- /dev/null
+/*
+ * Copyright (c) 2018 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.netconf.mdsal.connector.ops;
+
+import com.google.common.base.Strings;
+import org.opendaylight.netconf.api.DocumentedException;
+import org.opendaylight.netconf.api.xml.XmlElement;
+import org.opendaylight.netconf.util.mapping.AbstractSingletonNetconfOperation;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+abstract class AbstractConfigOperation extends AbstractSingletonNetconfOperation {
+
+ protected AbstractConfigOperation(final String netconfSessionIdForReporting) {
+ super(netconfSessionIdForReporting);
+ }
+
+ protected static NodeList getElementsByTagName(final XmlElement parent, final String key) throws
+ DocumentedException {
+ final Element domParent = parent.getDomElement();
+ final NodeList elementsByTagName;
+
+ if (Strings.isNullOrEmpty(domParent.getPrefix())) {
+ elementsByTagName = domParent.getElementsByTagName(key);
+ } else {
+ elementsByTagName = domParent.getElementsByTagNameNS(parent.getNamespace(), key);
+ }
+
+ return elementsByTagName;
+ }
+}
package org.opendaylight.netconf.mdsal.connector.ops;
import com.google.common.base.Optional;
-import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import java.net.URI;
import java.net.URISyntaxException;
import org.opendaylight.netconf.api.NetconfDocumentedException;
import org.opendaylight.netconf.api.xml.XmlElement;
import org.opendaylight.netconf.mdsal.connector.CurrentSchemaContext;
-import org.opendaylight.netconf.util.mapping.AbstractSingletonNetconfOperation;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
-abstract class AbstractEdit extends AbstractSingletonNetconfOperation {
+abstract class AbstractEdit extends AbstractConfigOperation {
private static final Logger LOG = LoggerFactory.getLogger(AbstractEdit.class);
private static final String TARGET_KEY = "target";
return childNode.get();
}
-
- protected static NodeList getElementsByTagName(final XmlElement parent, final String key) throws
- DocumentedException {
- final Element domParent = parent.getDomElement();
- final NodeList elementsByTagName;
-
- if (Strings.isNullOrEmpty(domParent.getPrefix())) {
- elementsByTagName = domParent.getElementsByTagName(key);
- } else {
- elementsByTagName = domParent.getElementsByTagNameNS(parent.getNamespace(), key);
- }
-
- return elementsByTagName;
- }
}
--- /dev/null
+/*
+ * Copyright (c) 2018 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.netconf.mdsal.connector.ops;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.opendaylight.netconf.api.DocumentedException;
+import org.opendaylight.netconf.api.DocumentedException.ErrorSeverity;
+import org.opendaylight.netconf.api.DocumentedException.ErrorTag;
+import org.opendaylight.netconf.api.DocumentedException.ErrorType;
+import org.opendaylight.netconf.api.xml.XmlElement;
+import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
+import org.opendaylight.netconf.api.xml.XmlUtil;
+import org.opendaylight.netconf.mdsal.connector.TransactionProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+public final class Validate extends AbstractConfigOperation {
+ private static final Logger LOG = LoggerFactory.getLogger(Validate.class);
+ private static final String OPERATION_NAME = "validate";
+ private static final String SOURCE_KEY = "source";
+
+ private final TransactionProvider transactionProvider;
+
+ public Validate(final String netconfSessionIdForReporting, final TransactionProvider transactionProvider) {
+ super(netconfSessionIdForReporting);
+ this.transactionProvider = transactionProvider;
+ }
+
+ @Override
+ protected Element handleWithNoSubsequentOperations(final Document document, final XmlElement operationElement)
+ throws DocumentedException {
+ final Datastore targetDatastore = extractSourceParameter(operationElement, OPERATION_NAME);
+ if (targetDatastore != Datastore.candidate) {
+ throw new DocumentedException("<validate> is only supported on candidate datastore",
+ DocumentedException.ErrorType.PROTOCOL,
+ ErrorTag.OPERATION_NOT_SUPPORTED,
+ ErrorSeverity.ERROR);
+ }
+
+ transactionProvider.validateTransaction();
+ LOG.trace("<validate> request completed successfully on session {}", getNetconfSessionIdForReporting());
+ return XmlUtil.createElement(document, XmlNetconfConstants.OK, Optional.absent());
+ }
+
+ protected static Datastore extractSourceParameter(final XmlElement operationElement, final String operationName)
+ throws DocumentedException {
+ final NodeList elementsByTagName = getElementsByTagName(operationElement, SOURCE_KEY);
+ // Direct lookup instead of using XmlElement class due to performance
+ if (elementsByTagName.getLength() == 0) {
+ final Map<String, String> errorInfo = ImmutableMap.of("bad-attribute", SOURCE_KEY, "bad-element",
+ operationName);
+ throw new DocumentedException("Missing source element", ErrorType.PROTOCOL, ErrorTag.MISSING_ELEMENT,
+ ErrorSeverity.ERROR, errorInfo);
+ } else if (elementsByTagName.getLength() > 1) {
+ throw new DocumentedException("Multiple source elements", ErrorType.RPC, ErrorTag.UNKNOWN_ATTRIBUTE,
+ ErrorSeverity.ERROR);
+ } else {
+ final XmlElement sourceChildNode =
+ XmlElement.fromDomElement((Element) elementsByTagName.item(0)).getOnlyChildElement();
+ return Datastore.valueOf(sourceChildNode.getName());
+ }
+ }
+
+ @Override
+ protected String getOperationName() {
+ return OPERATION_NAME;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2018 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.netconf.mdsal.connector.ops;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.MockitoAnnotations.initMocks;
+import static org.opendaylight.netconf.mdsal.connector.ops.AbstractNetconfOperationTest.RPC_REPLY_OK;
+import static org.opendaylight.netconf.mdsal.connector.ops.AbstractNetconfOperationTest.SESSION_ID_FOR_REPORTING;
+import static org.opendaylight.netconf.mdsal.connector.ops.AbstractNetconfOperationTest.executeOperation;
+import static org.opendaylight.netconf.mdsal.connector.ops.AbstractNetconfOperationTest.verifyResponse;
+
+import com.google.common.util.concurrent.Futures;
+import java.util.Collections;
+import org.custommonkey.xmlunit.XMLUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker;
+import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
+import org.opendaylight.netconf.api.DocumentedException;
+import org.opendaylight.netconf.mdsal.connector.DOMDataTransactionValidator;
+import org.opendaylight.netconf.mdsal.connector.DOMDataTransactionValidator.ValidationFailedException;
+import org.opendaylight.netconf.mdsal.connector.TransactionProvider;
+import org.w3c.dom.Document;
+
+public class ValidateTest {
+ @Mock
+ private DOMDataTransactionValidator noopValidator;
+ @Mock
+ private DOMDataTransactionValidator failingValidator;
+ @Mock
+ private DOMDataReadWriteTransaction readWriteTx;
+ @Mock
+ private DOMDataBroker dataBroker;
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+ doReturn(Futures.immediateCheckedFuture(null)).when(noopValidator).validate(any());
+ doReturn(Futures.immediateFailedCheckedFuture(new ValidationFailedException("invalid data")))
+ .when(failingValidator).validate(any());
+ doReturn(readWriteTx).when(dataBroker).newReadWriteTransaction();
+ XMLUnit.setIgnoreWhitespace(true);
+ }
+
+ @Test
+ public void testValidateUnsupported() throws Exception {
+ whenValidatorIsNotDefined();
+ try {
+ validate("messages/mapping/validate/validate.xml");
+ fail("Should have failed - <validate> not supported");
+ } catch (final DocumentedException e) {
+ assertEquals(DocumentedException.ErrorSeverity.ERROR, e.getErrorSeverity());
+ assertEquals(DocumentedException.ErrorTag.OPERATION_NOT_SUPPORTED, e.getErrorTag());
+ assertEquals(DocumentedException.ErrorType.PROTOCOL, e.getErrorType());
+ }
+ }
+
+ @Test
+ public void testSourceMissing() throws Exception {
+ whenUsingValidator(noopValidator);
+ try {
+ validate("messages/mapping/validate/validate_no_source.xml");
+ fail("Should have failed - <source> element is missing");
+ } catch (final DocumentedException e) {
+ assertEquals(DocumentedException.ErrorSeverity.ERROR, e.getErrorSeverity());
+ assertEquals(DocumentedException.ErrorTag.MISSING_ELEMENT, e.getErrorTag());
+ assertEquals(DocumentedException.ErrorType.PROTOCOL, e.getErrorType());
+ }
+ }
+
+ @Test
+ public void testSourceRunning() throws Exception {
+ whenUsingValidator(noopValidator);
+ try {
+ validate("messages/mapping/validate/validate_running.xml");
+ fail("Should have failed - <running/> is not supported");
+ } catch (final DocumentedException e) {
+ assertEquals(DocumentedException.ErrorSeverity.ERROR, e.getErrorSeverity());
+ assertEquals(DocumentedException.ErrorTag.OPERATION_NOT_SUPPORTED, e.getErrorTag());
+ assertEquals(DocumentedException.ErrorType.PROTOCOL, e.getErrorType());
+ }
+ }
+
+ @Test
+ public void testValidateEmptyTx() throws Exception {
+ whenUsingValidator(noopValidator);
+ verifyResponse(validate("messages/mapping/validate/validate.xml"), RPC_REPLY_OK);
+ verifyZeroInteractions(noopValidator);
+ }
+
+ @Test
+ public void testValidate() throws Exception {
+ whenUsingValidator(noopValidator);
+ final TransactionProvider transactionProvider = initCandidateTransaction();
+ verifyResponse(validate("messages/mapping/validate/validate.xml", transactionProvider), RPC_REPLY_OK);
+ verify(noopValidator).validate(readWriteTx);
+ }
+
+ @Test
+ public void testValidateFailed() throws Exception {
+ whenUsingValidator(failingValidator);
+ final TransactionProvider transactionProvider = initCandidateTransaction();
+ try {
+ validate("messages/mapping/validate/validate.xml", transactionProvider);
+ fail("Should have failed - operation failed");
+ } catch (final DocumentedException e) {
+ assertEquals(DocumentedException.ErrorSeverity.ERROR, e.getErrorSeverity());
+ assertEquals(DocumentedException.ErrorTag.OPERATION_FAILED, e.getErrorTag());
+ assertEquals(DocumentedException.ErrorType.APPLICATION, e.getErrorType());
+ }
+ }
+
+ private void whenValidatorIsNotDefined() {
+ doReturn(Collections.emptyMap()).when(dataBroker).getSupportedExtensions();
+ }
+
+ private void whenUsingValidator(final DOMDataTransactionValidator validator) {
+ doReturn(Collections.singletonMap(DOMDataTransactionValidator.class, validator))
+ .when(dataBroker).getSupportedExtensions();
+ }
+
+ private TransactionProvider initCandidateTransaction() {
+ final TransactionProvider transactionProvider = new TransactionProvider(dataBroker, SESSION_ID_FOR_REPORTING);
+ transactionProvider.getOrCreateTransaction();
+ return transactionProvider;
+ }
+
+ private Document validate(final String resource,
+ final TransactionProvider transactionProvider) throws Exception {
+ final Validate validate = new Validate(SESSION_ID_FOR_REPORTING, transactionProvider);
+ return executeOperation(validate, resource);
+ }
+
+ private Document validate(final String resource) throws Exception {
+ return validate(resource, new TransactionProvider(dataBroker, SESSION_ID_FOR_REPORTING));
+ }
+}
\ No newline at end of file
--- /dev/null
+<!--
+ ~ Copyright (c) 2018 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
+ -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+ <validate>
+ <source>
+ <candidate/>
+ </source>
+ </validate>
+</rpc>
\ No newline at end of file
--- /dev/null
+<!--
+ ~ Copyright (c) 2018 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
+ -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+ <validate/>
+</rpc>
\ No newline at end of file
--- /dev/null
+<!--
+ ~ Copyright (c) 2018 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
+ -->
+
+<rpc message-id="a" a="64" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+ <validate>
+ <source>
+ <running/>
+ </source>
+ </validate>
+</rpc>
\ No newline at end of file