/* * Copyright (c) 2020 PANTHEON.tech, s.r.o. 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.restconf.nb.rfc8040.rests.transactions; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Set; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.mdsal.common.api.CommitInfo; import org.opendaylight.mdsal.common.api.LogicalDatastoreType; import org.opendaylight.mdsal.common.api.ReadFailedException; import org.opendaylight.mdsal.dom.api.DOMActionService; import org.opendaylight.mdsal.dom.api.DOMMountPointService; import org.opendaylight.mdsal.dom.api.DOMRpcService; import org.opendaylight.mdsal.dom.api.DOMSchemaService.YangTextSourceExtension; import org.opendaylight.mdsal.dom.api.DOMTransactionChain; import org.opendaylight.netconf.dom.api.NetconfDataTreeService; import org.opendaylight.restconf.api.query.ContentParam; import org.opendaylight.restconf.api.query.FieldsParam; import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector; import org.opendaylight.restconf.api.query.WithDefaultsParam; import org.opendaylight.restconf.common.errors.RestconfDocumentedException; import org.opendaylight.restconf.common.errors.RestconfFuture; import org.opendaylight.restconf.common.errors.SettableRestconfFuture; import org.opendaylight.restconf.server.api.DataGetParams; import org.opendaylight.restconf.server.api.DataGetResult; import org.opendaylight.restconf.server.api.DatabindContext; import org.opendaylight.restconf.server.api.DatabindPath.Data; import org.opendaylight.restconf.server.api.ServerRequest; import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory; import org.opendaylight.yangtools.yang.common.Empty; import org.opendaylight.yangtools.yang.common.ErrorTag; import org.opendaylight.yangtools.yang.common.ErrorType; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.common.QNameModule; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; import org.opendaylight.yangtools.yang.data.util.DataSchemaContext; import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin; import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; /** * Implementation of RESTCONF operations on top of a raw NETCONF backend. * * @see NetconfDataTreeService */ public final class NetconfRestconfStrategy extends RestconfStrategy { private final NetconfDataTreeService netconfService; public NetconfRestconfStrategy(final DatabindContext databind, final NetconfDataTreeService netconfService, final @Nullable DOMRpcService rpcService, final @Nullable DOMActionService actionService, final @Nullable YangTextSourceExtension sourceProvider, final @Nullable DOMMountPointService mountPointService) { super(databind, ImmutableMap.of(), rpcService, actionService, sourceProvider, mountPointService); this.netconfService = requireNonNull(netconfService); } @Override RestconfTransaction prepareWriteExecution() { return new NetconfRestconfTransaction(databind(), netconfService); } @Override void delete(final SettableRestconfFuture future, final ServerRequest request, final YangInstanceIdentifier path) { final var tx = prepareWriteExecution(); tx.delete(path); Futures.addCallback(tx.commit(), new FutureCallback() { @Override public void onSuccess(final CommitInfo result) { future.set(Empty.value()); } @Override public void onFailure(final Throwable cause) { future.setFailure(TransactionUtil.decodeException(cause, "DELETE", path, modelContext())); } }, MoreExecutors.directExecutor()); } @Override RestconfFuture dataGET(final ServerRequest request, final Data path, final DataGetParams params) { final var fields = params.fields(); final List fieldPaths; if (fields != null) { final List tmp; try { tmp = fieldsParamToPaths(path.inference().modelContext(), path.schema(), fields); } catch (RestconfDocumentedException e) { return RestconfFuture.failed(e); } fieldPaths = tmp.isEmpty() ? null : tmp; } else { fieldPaths = null; } final NormalizedNode node; if (fieldPaths != null) { node = readData(params.content(), path.instance(), params.withDefaults(), fieldPaths); } else { node = readData(params.content(), path.instance(), params.withDefaults()); } return completeDataGET(node, path, NormalizedNodeWriterFactory.of(params.depth()), null); } @Override ListenableFuture> read(final LogicalDatastoreType store, final YangInstanceIdentifier path) { return switch (store) { case CONFIGURATION -> netconfService.getConfig(path); case OPERATIONAL -> netconfService.get(path); }; } private ListenableFuture> read(final LogicalDatastoreType store, final YangInstanceIdentifier path, final List fields) { return switch (store) { case CONFIGURATION -> netconfService.getConfig(path, fields); case OPERATIONAL -> netconfService.get(path, fields); }; } /** * Read specific type of data from data store via transaction with specified subtrees that should only be read. * Close {@link DOMTransactionChain} inside of object {@link RestconfStrategy} provided as a parameter. * * @param content type of data to read (config, state, all) * @param path the parent path to read * @param withDefa value of with-defaults parameter * @param fields paths to selected subtrees which should be read, relative to the parent path * @return {@link NormalizedNode} */ // FIXME: NETCONF-1155: this method should asynchronous public @Nullable NormalizedNode readData(final @NonNull ContentParam content, final @NonNull YangInstanceIdentifier path, final @Nullable WithDefaultsParam withDefa, final @NonNull List fields) { return switch (content) { case ALL -> { // PREPARE STATE DATA NODE final var stateDataNode = readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields); // PREPARE CONFIG DATA NODE final var configDataNode = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields); yield mergeConfigAndSTateDataIfNeeded(stateDataNode, withDefa == null ? configDataNode : prepareDataByParamWithDef(configDataNode, path, withDefa.mode())); } case CONFIG -> { final var read = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields); yield withDefa == null ? read : prepareDataByParamWithDef(read, path, withDefa.mode()); } case NONCONFIG -> readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields); }; } /** * Read specific type of data {@link LogicalDatastoreType} via transaction in {@link RestconfStrategy} with * specified subtrees that should only be read. * * @param store datastore type * @param path parent path to selected fields * @param fields paths to selected subtrees which should be read, relative to the parent path * @return {@link NormalizedNode} */ private @Nullable NormalizedNode readDataViaTransaction(final @NonNull LogicalDatastoreType store, final @NonNull YangInstanceIdentifier path, final @NonNull List fields) { return TransactionUtil.syncAccess(read(store, path, fields), path).orElse(null); } @Override ListenableFuture exists(final YangInstanceIdentifier path) { return Futures.transform(remapException(netconfService.getConfig(path)), optionalNode -> optionalNode != null && optionalNode.isPresent(), MoreExecutors.directExecutor()); } private static ListenableFuture remapException(final ListenableFuture input) { final var ret = SettableFuture.create(); Futures.addCallback(input, new FutureCallback<>() { @Override public void onSuccess(final T result) { ret.set(result); } @Override public void onFailure(final Throwable cause) { ret.setException(cause instanceof ReadFailedException ? cause : new ReadFailedException("NETCONF operation failed", cause)); } }, MoreExecutors.directExecutor()); return ret; } /** * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with * {@link NetconfDataTreeService}. * *

* Fields parser that stores a set of all the leaf {@link LinkedPathElement}s specified in {@link FieldsParam}. * Using {@link LinkedPathElement} it is possible to create a chain of path arguments and build complete paths * since this element contains identifiers of intermediary mixin nodes and also linked to its parent * {@link LinkedPathElement}. * *

* Example: field 'a(b/c;d/e)' ('e' is place under choice node 'x') is parsed into following levels: *

     *   - './a' +- 'a/b' - 'b/c'
     *           |
     *           +- 'a/d' - 'd/x/e'
     * 
* * * @param modelContext EffectiveModelContext * @param startNode Root DataSchemaNode * @param input input value of fields parameter * @return {@link List} of {@link YangInstanceIdentifier} that are relative to the last {@link PathArgument} * of provided {@code identifier} */ @VisibleForTesting static @NonNull List fieldsParamToPaths(final @NonNull EffectiveModelContext modelContext, final @NonNull DataSchemaContext startNode, final @NonNull FieldsParam input) { final var parsed = new HashSet(); processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(), new LinkedPathElement(null, List.of(), startNode), input.nodeSelectors()); return parsed.stream().map(NetconfRestconfStrategy::buildPath).toList(); } private static void processSelectors(final Set parsed, final EffectiveModelContext context, final QNameModule startNamespace, final LinkedPathElement startPathElement, final List selectors) { for (var selector : selectors) { var pathElement = startPathElement; var namespace = startNamespace; // Note: path is guaranteed to have at least one step final var it = selector.path().iterator(); do { final var step = it.next(); final var module = step.module(); if (module != null) { // FIXME: this is not defensive enough, as we can fail to find the module namespace = context.findModules(module).iterator().next().getQNameModule(); } // add parsed path element linked to its parent pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace)); } while (it.hasNext()); final var subs = selector.subSelectors(); if (!subs.isEmpty()) { processSelectors(parsed, context, namespace, pathElement, subs); } else { parsed.add(pathElement); } } } private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement, final QName childQName) { final var collectedMixinNodes = new ArrayList(); DataSchemaContext currentNode = currentElement.targetNode; DataSchemaContext actualContextNode = childByQName(currentNode, childQName); if (actualContextNode == null) { actualContextNode = resolveMixinNode(currentNode, currentNode.getPathStep().getNodeType()); actualContextNode = childByQName(actualContextNode, childQName); } while (actualContextNode != null && actualContextNode instanceof PathMixin) { final var actualDataSchemaNode = actualContextNode.dataSchemaNode(); if (actualDataSchemaNode instanceof ListSchemaNode listSchema && listSchema.getKeyDefinition().isEmpty()) { // we need just a single node identifier from list in the path IFF it is an unkeyed list, otherwise // we need both (which is the default case) actualContextNode = childByQName(actualContextNode, childQName); } else if (actualDataSchemaNode instanceof LeafListSchemaNode) { // NodeWithValue is unusable - stop parsing break; } else { collectedMixinNodes.add(actualContextNode.getPathStep()); actualContextNode = childByQName(actualContextNode, childQName); } } if (actualContextNode == null) { throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in " + currentNode.getPathStep().getNodeType().getLocalName(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); } return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode); } private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) { return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null; } private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) { var pathElement = lastPathElement; final var path = new LinkedList(); do { path.addFirst(contextPathArgument(pathElement.targetNode)); path.addAll(0, pathElement.mixinNodesToTarget); pathElement = pathElement.parentPathElement; } while (pathElement.parentPathElement != null); return YangInstanceIdentifier.of(path); } private static @NonNull PathArgument contextPathArgument(final DataSchemaContext context) { final var arg = context.pathStep(); if (arg != null) { return arg; } final var schema = context.dataSchemaNode(); if (schema instanceof ListSchemaNode listSchema && !listSchema.getKeyDefinition().isEmpty()) { return NodeIdentifierWithPredicates.of(listSchema.getQName()); } if (schema instanceof LeafListSchemaNode leafListSchema) { return new NodeWithValue<>(leafListSchema.getQName(), Empty.value()); } throw new UnsupportedOperationException("Unsupported schema " + schema); } private static DataSchemaContext resolveMixinNode(final DataSchemaContext node, final @NonNull QName qualifiedName) { DataSchemaContext currentNode = node; while (currentNode != null && currentNode instanceof PathMixin currentMixin) { currentNode = currentMixin.childByQName(qualifiedName); } return currentNode; } /** * {@link DataSchemaContext} of data element grouped with identifiers of leading mixin nodes and previous path * element.
* - identifiers of mixin nodes on the path to the target node - required for construction of full valid * DOM paths,
* - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain * of {@link PathArgument}s * * @param parentPathElement parent path element * @param mixinNodesToTarget identifiers of mixin nodes on the path to the target node * @param targetNode target non-mixin node */ private record LinkedPathElement( @Nullable LinkedPathElement parentPathElement, @NonNull List mixinNodesToTarget, @NonNull DataSchemaContext targetNode) { LinkedPathElement { requireNonNull(mixinNodesToTarget); requireNonNull(targetNode); } } }