NETCONF-557: Add support for URL capability
[netconf.git] / netconf / mdsal-netconf-connector / src / main / java / org / opendaylight / netconf / mdsal / connector / ops / CopyConfig.java
1 /*
2  * Copyright (c) 2018 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8
9 package org.opendaylight.netconf.mdsal.connector.ops;
10
11 import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0;
12
13 import com.google.common.base.Optional;
14 import java.io.IOException;
15 import java.net.URI;
16 import java.net.URISyntaxException;
17 import java.nio.charset.StandardCharsets;
18 import java.nio.file.Files;
19 import java.nio.file.Path;
20 import java.nio.file.Paths;
21 import java.util.List;
22 import javax.xml.stream.XMLOutputFactory;
23 import javax.xml.stream.XMLStreamException;
24 import javax.xml.stream.XMLStreamWriter;
25 import javax.xml.transform.dom.DOMResult;
26 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
27 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
28 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
29 import org.opendaylight.netconf.api.DocumentedException;
30 import org.opendaylight.netconf.api.DocumentedException.ErrorSeverity;
31 import org.opendaylight.netconf.api.DocumentedException.ErrorTag;
32 import org.opendaylight.netconf.api.DocumentedException.ErrorType;
33 import org.opendaylight.netconf.api.xml.XmlElement;
34 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
35 import org.opendaylight.netconf.api.xml.XmlUtil;
36 import org.opendaylight.netconf.mdsal.connector.CurrentSchemaContext;
37 import org.opendaylight.netconf.mdsal.connector.TransactionProvider;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
39 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
40 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
41 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
42 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
43 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
44 import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
45 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
46 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
47 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
48 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
50 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
51 import org.w3c.dom.Document;
52 import org.w3c.dom.Element;
53 import org.w3c.dom.Node;
54
55 public final class CopyConfig extends AbstractEdit {
56     private static final String OPERATION_NAME = "copy-config";
57     private static final String SOURCE_KEY = "source";
58     private static final XMLOutputFactory XML_OUTPUT_FACTORY;
59
60     static {
61         XML_OUTPUT_FACTORY = XMLOutputFactory.newFactory();
62         XML_OUTPUT_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
63     }
64
65     // Top-level "data" node without child nodes
66     private static final ContainerNode EMPTY_ROOT_NODE = Builders.containerBuilder()
67         .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(SchemaContext.NAME)).build();
68
69     private final TransactionProvider transactionProvider;
70
71     public CopyConfig(final String netconfSessionIdForReporting, final CurrentSchemaContext schemaContext,
72                       final TransactionProvider transactionProvider) {
73         super(netconfSessionIdForReporting, schemaContext);
74         this.transactionProvider = transactionProvider;
75     }
76
77     @Override
78     protected Element handleWithNoSubsequentOperations(final Document document, final XmlElement operationElement)
79         throws DocumentedException {
80         final XmlElement targetElement = extractTargetElement(operationElement, OPERATION_NAME);
81         final String target = targetElement.getName();
82         if (Datastore.running.toString().equals(target)) {
83             throw new DocumentedException("edit-config on running datastore is not supported",
84                 ErrorType.PROTOCOL,
85                 ErrorTag.OPERATION_NOT_SUPPORTED,
86                 ErrorSeverity.ERROR);
87         } else if (Datastore.candidate.toString().equals(target)) {
88             copyToCandidate(operationElement);
89         } else if (URL_KEY.equals(target)) {
90             copyToUrl(targetElement, operationElement);
91         } else {
92             throw new DocumentedException("Unsupported target: " + target,
93                 ErrorType.PROTOCOL,
94                 ErrorTag.BAD_ELEMENT,
95                 ErrorSeverity.ERROR);
96         }
97         return XmlUtil.createElement(document, XmlNetconfConstants.OK, Optional.absent());
98     }
99
100     private void copyToCandidate(final XmlElement operationElement)
101         throws DocumentedException {
102         final XmlElement source = getSourceElement(operationElement);
103         final List<XmlElement> configElements = getConfigElement(source).getChildElements();
104
105         // <copy-config>, unlike <edit-config>, always replaces entire configuration,
106         // so remove old configuration first:
107         final DOMDataReadWriteTransaction rwTx = transactionProvider.getOrCreateTransaction();
108         rwTx.put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.EMPTY, EMPTY_ROOT_NODE);
109
110         // Then create nodes present in the <config> element:
111         for (final XmlElement element : configElements) {
112             final String ns = element.getNamespace();
113             final DataSchemaNode schemaNode = getSchemaNodeFromNamespace(ns, element);
114             final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
115             parseIntoNormalizedNode(schemaNode, element, ImmutableNormalizedNodeStreamWriter.from(resultHolder));
116             final NormalizedNode<?, ?> data = resultHolder.getResult();
117             final YangInstanceIdentifier path = YangInstanceIdentifier.create(data.getIdentifier());
118             // Doing merge instead of put to support top-level list:
119             rwTx.merge(LogicalDatastoreType.CONFIGURATION, path, data);
120         }
121     }
122
123     private static XmlElement getSourceElement(final XmlElement parent) throws DocumentedException {
124         final Optional<XmlElement> sourceElement = parent.getOnlyChildElementOptionally(SOURCE_KEY);
125         if (!sourceElement.isPresent()) {
126             throw new DocumentedException("<source> element is missing",
127                 DocumentedException.ErrorType.PROTOCOL,
128                 DocumentedException.ErrorTag.MISSING_ELEMENT,
129                 DocumentedException.ErrorSeverity.ERROR);
130         }
131
132         return sourceElement.get();
133     }
134
135     private void copyToUrl(final XmlElement urlElement, final XmlElement operationElement) throws DocumentedException {
136         final String url = urlElement.getTextContent();
137         if (!url.startsWith("file:")) {
138             throw new DocumentedException("Unsupported <url> protocol: " + url,
139                 ErrorType.PROTOCOL,
140                 ErrorTag.OPERATION_NOT_SUPPORTED,
141                 ErrorSeverity.ERROR);
142         }
143
144         // Read data from datastore:
145         final XmlElement source = getSourceElement(operationElement).getOnlyChildElement();
146         final ContainerNode data = readData(source);
147
148         // Transform NN to XML:
149         final Document document = operationElement.getDomElement().getOwnerDocument();
150         final Node node = transformNormalizedNode(document, data);
151
152         // Save XML to file:
153         final String xml = XmlUtil.toString((Element) node);
154         try {
155             final Path file = Paths.get(new URI(url));
156             Files.write(file, xml.getBytes(StandardCharsets.UTF_8));
157         } catch (URISyntaxException | IllegalArgumentException e) {
158             throw new DocumentedException("Invalid URI: " + url, e,
159                 ErrorType.RPC,
160                 ErrorTag.INVALID_VALUE,
161                 ErrorSeverity.ERROR);
162         } catch (IOException e) {
163             throw new DocumentedException("Failed to write : " + url, e,
164                 ErrorType.APPLICATION,
165                 ErrorTag.OPERATION_FAILED,
166                 ErrorSeverity.ERROR);
167         }
168     }
169
170     private ContainerNode readData(final XmlElement source) throws DocumentedException {
171         final Datastore sourceDatastore = getDatastore(source);
172         final DOMDataReadWriteTransaction rwTx = getTransaction(sourceDatastore);
173         final YangInstanceIdentifier dataRoot = YangInstanceIdentifier.EMPTY;
174         try {
175             final Optional<NormalizedNode<?, ?>> normalizedNodeOptional = rwTx.read(
176                 LogicalDatastoreType.CONFIGURATION, dataRoot).checkedGet();
177             if (sourceDatastore == Datastore.running) {
178                 transactionProvider.abortRunningTransaction(rwTx);
179             }
180             return (ContainerNode) normalizedNodeOptional.get();
181         } catch (ReadFailedException e) {
182             throw new IllegalStateException("Unable to read data " + dataRoot, e);
183         }
184     }
185
186     private static Datastore getDatastore(final XmlElement source) throws DocumentedException {
187         try {
188             return Datastore.valueOf(source.getName());
189         } catch (IllegalArgumentException e) {
190             throw new DocumentedException("Unsupported source for <url> target", e,
191                 ErrorType.PROTOCOL,
192                 ErrorTag.OPERATION_NOT_SUPPORTED,
193                 ErrorSeverity.ERROR);
194         }
195     }
196
197     private DOMDataReadWriteTransaction getTransaction(final Datastore datastore) throws DocumentedException {
198         if (datastore == Datastore.candidate) {
199             return transactionProvider.getOrCreateTransaction();
200         } else if (datastore == Datastore.running) {
201             return transactionProvider.createRunningTransaction();
202         }
203         throw new DocumentedException("Incorrect Datastore: ", ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT,
204             ErrorSeverity.ERROR);
205     }
206
207     private Node transformNormalizedNode(final Document document, final ContainerNode data) {
208         final Element configElement = document.createElementNS(URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0, CONFIG_KEY);
209         final DOMResult result = new DOMResult(configElement);
210         try {
211             final XMLStreamWriter xmlWriter = XML_OUTPUT_FACTORY.createXMLStreamWriter(result);
212             final NormalizedNodeStreamWriter nnStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
213                 schemaContext.getCurrentContext(), SchemaPath.ROOT);
214
215             final NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(nnStreamWriter, true);
216             for (DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?> child : data.getValue()) {
217                 nnWriter.write(child);
218             }
219             nnWriter.flush();
220             xmlWriter.flush();
221         } catch (XMLStreamException | IOException e) {
222             throw new RuntimeException(e);
223         }
224         return result.getNode();
225     }
226
227     @Override
228     protected String getOperationName() {
229         return OPERATION_NAME;
230     }
231 }