2 * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.server.mdsal;
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.annotations.VisibleForTesting;
14 import com.google.common.collect.HashBasedTable;
15 import com.google.common.collect.ImmutableMap;
16 import com.google.common.collect.ImmutableSet;
17 import com.google.common.collect.ImmutableSetMultimap;
18 import com.google.common.collect.Maps;
19 import com.google.common.util.concurrent.FutureCallback;
20 import com.google.common.util.concurrent.Futures;
21 import com.google.common.util.concurrent.MoreExecutors;
22 import java.io.IOException;
23 import java.lang.invoke.MethodHandles;
24 import java.lang.invoke.VarHandle;
26 import java.util.Comparator;
27 import java.util.List;
29 import java.util.Map.Entry;
30 import java.util.Optional;
31 import java.util.concurrent.CancellationException;
32 import javax.inject.Inject;
33 import javax.inject.Singleton;
34 import org.eclipse.jdt.annotation.NonNull;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
37 import org.opendaylight.mdsal.dom.api.DOMActionException;
38 import org.opendaylight.mdsal.dom.api.DOMActionResult;
39 import org.opendaylight.mdsal.dom.api.DOMActionService;
40 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
41 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
42 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
43 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
44 import org.opendaylight.mdsal.dom.api.DOMRpcService;
45 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
46 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
47 import org.opendaylight.restconf.common.errors.RestconfFuture;
48 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
49 import org.opendaylight.restconf.common.patch.PatchContext;
50 import org.opendaylight.restconf.common.patch.PatchStatusContext;
51 import org.opendaylight.restconf.nb.rfc8040.Insert;
52 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
53 import org.opendaylight.restconf.nb.rfc8040.databind.ChildBody;
54 import org.opendaylight.restconf.nb.rfc8040.databind.DataPostBody;
55 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
56 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
57 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
58 import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
59 import org.opendaylight.restconf.nb.rfc8040.databind.ResourceBody;
60 import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
61 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
62 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
63 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
64 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
65 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
66 import org.opendaylight.restconf.server.api.DataPostResult;
67 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
68 import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
69 import org.opendaylight.restconf.server.api.DataPutResult;
70 import org.opendaylight.restconf.server.api.OperationsGetResult;
71 import org.opendaylight.restconf.server.api.RestconfServer;
72 import org.opendaylight.restconf.server.spi.OperationInput;
73 import org.opendaylight.restconf.server.spi.OperationOutput;
74 import org.opendaylight.restconf.server.spi.RpcImplementation;
75 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.YangApi;
76 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.restconf.Restconf;
77 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev190104.YangLibrary;
78 import org.opendaylight.yangtools.yang.common.Empty;
79 import org.opendaylight.yangtools.yang.common.ErrorTag;
80 import org.opendaylight.yangtools.yang.common.ErrorType;
81 import org.opendaylight.yangtools.yang.common.QName;
82 import org.opendaylight.yangtools.yang.common.QNameModule;
83 import org.opendaylight.yangtools.yang.common.Revision;
84 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
85 import org.opendaylight.yangtools.yang.common.XMLNamespace;
86 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
87 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
88 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
89 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
90 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
91 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
92 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
93 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
94 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
95 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
96 import org.osgi.service.component.annotations.Activate;
97 import org.osgi.service.component.annotations.Component;
98 import org.osgi.service.component.annotations.Reference;
99 import org.slf4j.Logger;
100 import org.slf4j.LoggerFactory;
103 * A RESTCONF server implemented on top of MD-SAL.
107 public final class MdsalRestconfServer implements RestconfServer {
108 private static final Logger LOG = LoggerFactory.getLogger(MdsalRestconfServer.class);
109 private static final QName YANG_LIBRARY_VERSION = QName.create(Restconf.QNAME, "yang-library-version").intern();
110 private static final String YANG_LIBRARY_REVISION = YangLibrary.QNAME.getRevision().orElseThrow().toString();
111 private static final VarHandle LOCAL_STRATEGY;
115 LOCAL_STRATEGY = MethodHandles.lookup()
116 .findVarHandle(MdsalRestconfServer.class, "localStrategy", RestconfStrategy.class);
117 } catch (NoSuchFieldException | IllegalAccessException e) {
118 throw new ExceptionInInitializerError(e);
122 private final @NonNull ImmutableMap<QName, RpcImplementation> localRpcs;
123 private final @NonNull DOMMountPointService mountPointService;
124 private final @NonNull DatabindProvider databindProvider;
125 private final @NonNull DOMDataBroker dataBroker;
126 private final @Nullable DOMRpcService rpcService;
127 private final @Nullable DOMActionService actionService;
129 @SuppressWarnings("unused")
130 private volatile RestconfStrategy localStrategy;
134 public MdsalRestconfServer(@Reference final DatabindProvider databindProvider,
135 @Reference final DOMDataBroker dataBroker, @Reference final DOMRpcService rpcService,
136 @Reference final DOMActionService actionService,
137 @Reference final DOMMountPointService mountPointService,
138 @Reference final List<RpcImplementation> localRpcs) {
139 this.databindProvider = requireNonNull(databindProvider);
140 this.dataBroker = requireNonNull(dataBroker);
141 this.rpcService = requireNonNull(rpcService);
142 this.actionService = requireNonNull(actionService);
143 this.mountPointService = requireNonNull(mountPointService);
144 this.localRpcs = Maps.uniqueIndex(localRpcs, RpcImplementation::qname);
147 public MdsalRestconfServer(final DatabindProvider databindProvider, final DOMDataBroker dataBroker,
148 final DOMRpcService rpcService, final DOMActionService actionService,
149 final DOMMountPointService mountPointService, final RpcImplementation... localRpcs) {
150 this(databindProvider, dataBroker, rpcService, actionService, mountPointService, List.of(localRpcs));
154 public RestconfFuture<Empty> dataDELETE(final String identifier) {
155 final var reqPath = bindRequestPath(identifier);
156 final var strategy = getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
157 return strategy.delete(reqPath.getInstanceIdentifier());
161 public RestconfFuture<NormalizedNodePayload> dataGET(final ReadDataParams readParams) {
162 return readData(bindRequestRoot(), readParams);
166 public RestconfFuture<NormalizedNodePayload> dataGET(final String identifier, final ReadDataParams readParams) {
167 return readData(bindRequestPath(identifier), readParams);
170 private @NonNull RestconfFuture<NormalizedNodePayload> readData(final InstanceIdentifierContext reqPath,
171 final ReadDataParams readParams) {
172 final var queryParams = QueryParams.newQueryParameters(readParams, reqPath);
173 final var fieldPaths = queryParams.fieldPaths();
174 final var strategy = getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
175 final NormalizedNode node;
176 if (fieldPaths != null && !fieldPaths.isEmpty()) {
177 node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(), readParams.withDefaults(),
180 node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(), readParams.withDefaults());
183 return RestconfFuture.failed(new RestconfDocumentedException(
184 "Request could not be completed because the relevant data model content does not exist",
185 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
188 return RestconfFuture.of(new NormalizedNodePayload(reqPath.inference(), node, queryParams));
192 public RestconfFuture<Empty> dataPATCH(final ResourceBody body) {
193 return dataPATCH(bindRequestRoot(), body);
197 public RestconfFuture<Empty> dataPATCH(final String identifier, final ResourceBody body) {
198 return dataPATCH(bindRequestPath(identifier), body);
201 private @NonNull RestconfFuture<Empty> dataPATCH(final InstanceIdentifierContext reqPath, final ResourceBody body) {
202 final var req = bindResourceRequest(reqPath, body);
203 return req.strategy().merge(req.path(), req.data());
207 public RestconfFuture<PatchStatusContext> dataPATCH(final PatchBody body) {
208 return dataPATCH(bindRequestRoot(), body);
212 public RestconfFuture<PatchStatusContext> dataPATCH(final String identifier, final PatchBody body) {
213 return dataPATCH(bindRequestPath(identifier), body);
216 private @NonNull RestconfFuture<PatchStatusContext> dataPATCH(final InstanceIdentifierContext reqPath,
217 final PatchBody body) {
218 final var modelContext = reqPath.getSchemaContext();
219 final PatchContext patch;
221 patch = body.toPatchContext(modelContext, reqPath.getInstanceIdentifier());
222 } catch (IOException e) {
223 LOG.debug("Error parsing YANG Patch input", e);
224 return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
225 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
227 return getRestconfStrategy(modelContext, reqPath.getMountPoint()).patchData(patch);
231 public RestconfFuture<CreateResource> dataPOST(final ChildBody body, final Map<String, String> queryParameters) {
232 return dataCreatePOST(bindRequestRoot(), body, queryParameters);
236 public RestconfFuture<? extends DataPostResult> dataPOST(final String identifier, final DataPostBody body,
237 final Map<String, String> queryParameters) {
238 final var reqPath = bindRequestPath(identifier);
239 if (reqPath.getSchemaNode() instanceof ActionDefinition) {
240 try (var inputBody = body.toOperationInput()) {
241 return dataInvokePOST(reqPath, inputBody);
245 try (var childBody = body.toResource()) {
246 return dataCreatePOST(reqPath, childBody, queryParameters);
250 private @NonNull RestconfFuture<CreateResource> dataCreatePOST(final InstanceIdentifierContext reqPath,
251 final ChildBody body, final Map<String, String> queryParameters) {
252 final var inference = reqPath.inference();
253 final var modelContext = inference.getEffectiveModelContext();
257 insert = Insert.ofQueryParameters(modelContext, queryParameters);
258 } catch (IllegalArgumentException e) {
259 return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
260 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
263 final var parentPath = reqPath.getInstanceIdentifier();
264 final var payload = body.toPayload(parentPath, inference);
265 return getRestconfStrategy(modelContext, reqPath.getMountPoint())
266 .postData(concat(parentPath, payload.prefix()), payload.body(), insert);
269 private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
271 for (var arg : args) {
277 private RestconfFuture<InvokeOperation> dataInvokePOST(final InstanceIdentifierContext reqPath,
278 final OperationInputBody body) {
279 final var yangIIdContext = reqPath.getInstanceIdentifier();
280 final var inference = reqPath.inference();
281 final ContainerNode input;
283 input = body.toContainerNode(inference);
284 } catch (IOException e) {
285 LOG.debug("Error reading input", e);
286 return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
287 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
290 final var mountPoint = reqPath.getMountPoint();
291 final var schemaPath = inference.toSchemaInferenceStack().toSchemaNodeIdentifier();
292 final var future = mountPoint != null ? dataInvokePOST(input, schemaPath, yangIIdContext, mountPoint)
293 : dataInvokePOST(input, schemaPath, yangIIdContext, actionService);
295 return future.transform(result -> result.getOutput()
296 .flatMap(output -> output.isEmpty() ? Optional.empty()
297 : Optional.of(new InvokeOperation(new NormalizedNodePayload(reqPath.inference(), output))))
298 .orElse(InvokeOperation.EMPTY));
302 * Invoke Action via ActionServiceHandler.
304 * @param data input data
305 * @param yangIId invocation context
306 * @param schemaPath schema path of data
307 * @param actionService action service to invoke action
308 * @return {@link DOMActionResult}
310 private static RestconfFuture<DOMActionResult> dataInvokePOST(final ContainerNode data, final Absolute schemaPath,
311 final YangInstanceIdentifier yangIId, final DOMActionService actionService) {
312 final var ret = new SettableRestconfFuture<DOMActionResult>();
314 Futures.addCallback(actionService.invokeAction(schemaPath,
315 new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data),
316 new FutureCallback<DOMActionResult>() {
318 public void onSuccess(final DOMActionResult result) {
319 final var errors = result.getErrors();
320 LOG.debug("InvokeAction Error Message {}", errors);
321 if (errors.isEmpty()) {
324 ret.setFailure(new RestconfDocumentedException("InvokeAction Error Message ", null, errors));
329 public void onFailure(final Throwable cause) {
330 if (cause instanceof DOMActionException) {
331 ret.set(new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
332 ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))));
333 } else if (cause instanceof RestconfDocumentedException e) {
335 } else if (cause instanceof CancellationException) {
336 ret.setFailure(new RestconfDocumentedException("Action cancelled while executing",
337 ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, cause));
339 ret.setFailure(new RestconfDocumentedException("Invocation failed", cause));
342 }, MoreExecutors.directExecutor());
348 * Invoking Action via mount point.
350 * @param mountPoint mount point
351 * @param data input data
352 * @param schemaPath schema path of data
353 * @return {@link DOMActionResult}
355 private static RestconfFuture<DOMActionResult> dataInvokePOST(final ContainerNode data, final Absolute schemaPath,
356 final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
357 final var actionService = mountPoint.getService(DOMActionService.class);
358 return actionService.isPresent() ? dataInvokePOST(data, schemaPath, yangIId, actionService.orElseThrow())
359 : RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
363 public RestconfFuture<DataPutResult> dataPUT(final ResourceBody body, final Map<String, String> query) {
364 return dataPUT(bindRequestRoot(), body, query);
368 public RestconfFuture<DataPutResult> dataPUT(final String identifier, final ResourceBody body,
369 final Map<String, String> queryParameters) {
370 return dataPUT(bindRequestPath(identifier), body, queryParameters);
373 private @NonNull RestconfFuture<DataPutResult> dataPUT(final InstanceIdentifierContext reqPath,
374 final ResourceBody body, final Map<String, String> queryParameters) {
377 insert = Insert.ofQueryParameters(reqPath.getSchemaContext(), queryParameters);
378 } catch (IllegalArgumentException e) {
379 return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
380 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
382 final var req = bindResourceRequest(reqPath, body);
383 return req.strategy().putData(req.path(), req.data(), insert);
387 public RestconfFuture<OperationsGetResult> operationsGET() {
388 return operationsGET(databindProvider.currentContext().modelContext());
392 public RestconfFuture<OperationsGetResult> operationsGET(final String operation) {
393 // get current module RPCs/actions by RPC/action name
394 final var inference = bindRequestPath(operation).inference();
395 if (inference.isEmpty()) {
396 return operationsGET(inference.getEffectiveModelContext());
399 final var stmt = inference.toSchemaInferenceStack().currentStatement();
400 if (stmt instanceof RpcEffectiveStatement rpc) {
401 return RestconfFuture.of(
402 new OperationsGetResult.Leaf(inference.getEffectiveModelContext(), rpc.argument()));
404 return RestconfFuture.failed(new RestconfDocumentedException("RPC not found",
405 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
408 private static @NonNull RestconfFuture<OperationsGetResult> operationsGET(
409 final EffectiveModelContext modelContext) {
410 final var modules = modelContext.getModuleStatements();
411 if (modules.isEmpty()) {
412 // No modules, or defensive return empty content
413 return RestconfFuture.of(new OperationsGetResult.Container(modelContext, ImmutableSetMultimap.of()));
416 // RPCs by their XMLNamespace/Revision
417 final var table = HashBasedTable.<XMLNamespace, Revision, ImmutableSet<QName>>create();
418 for (var entry : modules.entrySet()) {
419 final var module = entry.getValue();
420 final var rpcNames = module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
421 .map(RpcEffectiveStatement::argument)
422 .collect(ImmutableSet.toImmutableSet());
423 if (!rpcNames.isEmpty()) {
424 final var namespace = entry.getKey();
425 table.put(namespace.getNamespace(), namespace.getRevision().orElse(null), rpcNames);
429 // Now pick the latest revision for each namespace
430 final var rpcs = ImmutableSetMultimap.<QNameModule, QName>builder();
431 for (var entry : table.rowMap().entrySet()) {
432 entry.getValue().entrySet().stream()
433 .sorted(Comparator.comparing(Entry::getKey, (first, second) -> Revision.compare(second, first)))
435 .ifPresent(row -> rpcs.putAll(QNameModule.create(entry.getKey(), row.getKey()), row.getValue()));
437 return RestconfFuture.of(new OperationsGetResult.Container(modelContext, rpcs.build()));
441 public RestconfFuture<OperationOutput> operationsPOST(final URI restconfURI, final String apiPath,
442 final OperationInputBody body) {
443 final var currentContext = databindProvider.currentContext();
444 final var reqPath = bindRequestPath(currentContext, apiPath);
445 final var inference = reqPath.inference();
446 final ContainerNode input;
448 input = body.toContainerNode(inference);
449 } catch (IOException e) {
450 LOG.debug("Error reading input", e);
451 return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
452 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
455 return getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint())
456 .invokeRpc(restconfURI, reqPath.getSchemaNode().getQName(),
457 new OperationInput(currentContext, inference, input));
461 public RestconfFuture<NormalizedNodePayload> yangLibraryVersionGET() {
462 final var stack = SchemaInferenceStack.of(databindProvider.currentContext().modelContext());
464 stack.enterYangData(YangApi.NAME);
465 stack.enterDataTree(Restconf.QNAME);
466 stack.enterDataTree(YANG_LIBRARY_VERSION);
467 } catch (IllegalArgumentException e) {
468 return RestconfFuture.failed(new RestconfDocumentedException("RESTCONF is not available"));
470 return RestconfFuture.of(new NormalizedNodePayload(stack.toInference(),
471 ImmutableNodes.leafNode(YANG_LIBRARY_VERSION, YANG_LIBRARY_REVISION)));
474 private @NonNull InstanceIdentifierContext bindRequestPath(final String identifier) {
475 return bindRequestPath(databindProvider.currentContext(), identifier);
478 private @NonNull InstanceIdentifierContext bindRequestPath(final DatabindContext databind,
479 final String identifier) {
480 // FIXME: go through ApiPath first. That part should eventually live in callers
481 // FIXME: DatabindContext looks like it should be internal
482 return verifyNotNull(ParserIdentifier.toInstanceIdentifier(requireNonNull(identifier), databind.modelContext(),
486 private @NonNull InstanceIdentifierContext bindRequestRoot() {
487 return InstanceIdentifierContext.ofLocalRoot(databindProvider.currentContext().modelContext());
490 private @NonNull ResourceRequest bindResourceRequest(final InstanceIdentifierContext reqPath,
491 final ResourceBody body) {
492 final var inference = reqPath.inference();
493 final var path = reqPath.getInstanceIdentifier();
494 final var data = body.toNormalizedNode(path, inference, reqPath.getSchemaNode());
496 return new ResourceRequest(
497 getRestconfStrategy(inference.getEffectiveModelContext(), reqPath.getMountPoint()), path, data);
501 @NonNull RestconfStrategy getRestconfStrategy(final EffectiveModelContext modelContext,
502 final @Nullable DOMMountPoint mountPoint) {
503 if (mountPoint == null) {
504 return localStrategy(modelContext);
507 final var ret = RestconfStrategy.forMountPoint(modelContext, mountPoint);
509 final var mountId = mountPoint.getIdentifier();
510 LOG.warn("Mount point {} does not expose a suitable access interface", mountId);
511 throw new RestconfDocumentedException("Could not find a supported access interface in mount point",
512 ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, mountId);
517 private @NonNull RestconfStrategy localStrategy(final EffectiveModelContext modelContext) {
518 final var local = (RestconfStrategy) LOCAL_STRATEGY.getAcquire(this);
519 if (local != null && modelContext.equals(local.modelContext())) {
523 final var created = new MdsalRestconfStrategy(modelContext, dataBroker, rpcService, localRpcs);
524 LOCAL_STRATEGY.setRelease(this, created);