Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2017-present Open Networking Foundation |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | package org.onosproject.d.config.sync.impl.netconf; |
| 17 | |
| 18 | import static com.google.common.base.Preconditions.checkNotNull; |
| 19 | import static java.nio.charset.StandardCharsets.UTF_8; |
| 20 | import static java.util.concurrent.CompletableFuture.completedFuture; |
| 21 | import static org.slf4j.LoggerFactory.getLogger; |
| 22 | |
| 23 | import java.io.IOException; |
| 24 | import java.io.InputStreamReader; |
| 25 | import java.util.concurrent.CompletableFuture; |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 26 | import org.onlab.util.XmlString; |
| 27 | import org.onosproject.d.config.ResourceIds; |
| 28 | import org.onosproject.d.config.sync.DeviceConfigSynchronizationProvider; |
| 29 | import org.onosproject.d.config.sync.impl.netconf.NetconfDeviceConfigSynchronizerComponent.NetconfContext; |
| 30 | import org.onosproject.d.config.sync.operation.SetRequest; |
| 31 | import org.onosproject.d.config.sync.operation.SetRequest.Change; |
| 32 | import org.onosproject.d.config.sync.operation.SetRequest.Change.Operation; |
| 33 | import org.onosproject.d.config.sync.operation.SetResponse; |
| 34 | import org.onosproject.d.config.sync.operation.SetResponse.Code; |
| 35 | import org.onosproject.net.DeviceId; |
| 36 | import org.onosproject.net.provider.AbstractProvider; |
| 37 | import org.onosproject.net.provider.ProviderId; |
| 38 | import org.onosproject.netconf.NetconfDevice; |
| 39 | import org.onosproject.netconf.NetconfException; |
| 40 | import org.onosproject.netconf.NetconfSession; |
| 41 | import org.onosproject.yang.model.DataNode; |
| 42 | import org.onosproject.yang.model.DefaultResourceData; |
| 43 | import org.onosproject.yang.model.InnerNode; |
| 44 | import org.onosproject.yang.model.ResourceData; |
| 45 | import org.onosproject.yang.model.ResourceId; |
| 46 | import org.onosproject.yang.runtime.AnnotatedNodeInfo; |
| 47 | import org.onosproject.yang.runtime.Annotation; |
Yuta HIGUCHI | 9285d97 | 2017-10-16 16:15:00 -0700 | [diff] [blame] | 48 | import org.onosproject.yang.runtime.CompositeData; |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 49 | import org.onosproject.yang.runtime.CompositeStream; |
| 50 | import org.onosproject.yang.runtime.DefaultAnnotatedNodeInfo; |
| 51 | import org.onosproject.yang.runtime.DefaultAnnotation; |
| 52 | import org.onosproject.yang.runtime.DefaultCompositeData; |
| 53 | import org.onosproject.yang.runtime.DefaultRuntimeContext; |
| 54 | import org.onosproject.yang.runtime.RuntimeContext; |
| 55 | import org.slf4j.Logger; |
| 56 | import com.google.common.io.CharStreams; |
| 57 | |
| 58 | /** |
| 59 | * Dynamic config synchronizer provider for NETCONF. |
Yuta HIGUCHI | ab35080 | 2018-05-07 14:56:32 -0700 | [diff] [blame] | 60 | * |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 61 | * <ul> |
| 62 | * <li> Converts POJO YANG into XML. |
| 63 | * <li> Adds NETCONF envelope around it. |
| 64 | * <li> Send request down to the device over NETCONF |
| 65 | * </ul> |
| 66 | */ |
| 67 | public class NetconfDeviceConfigSynchronizerProvider |
| 68 | extends AbstractProvider |
| 69 | implements DeviceConfigSynchronizationProvider { |
| 70 | |
| 71 | private static final Logger log = getLogger(NetconfDeviceConfigSynchronizerProvider.class); |
| 72 | |
| 73 | // TODO this should probably be defined on YRT Serializer side |
| 74 | /** |
| 75 | * {@link RuntimeContext} parameter Dataformat specifying XML. |
| 76 | */ |
| 77 | private static final String DATAFORMAT_XML = "xml"; |
| 78 | |
| 79 | private static final String XMLNS_XC = "xmlns:xc"; |
| 80 | private static final String NETCONF_1_0_BASE_NAMESPACE = |
| 81 | "urn:ietf:params:xml:ns:netconf:base:1.0"; |
| 82 | |
| 83 | /** |
| 84 | * Annotation to add xc namespace declaration. |
| 85 | * {@value #XMLNS_XC}={@value #NETCONF_1_0_BASE_NAMESPACE} |
| 86 | */ |
| 87 | private static final DefaultAnnotation XMLNS_XC_ANNOTATION = |
| 88 | new DefaultAnnotation(XMLNS_XC, NETCONF_1_0_BASE_NAMESPACE); |
| 89 | |
| 90 | private static final String XC_OPERATION = "xc:operation"; |
| 91 | |
| 92 | |
| 93 | private NetconfContext context; |
| 94 | |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 95 | protected NetconfDeviceConfigSynchronizerProvider(ProviderId id, |
| 96 | NetconfContext context) { |
| 97 | super(id); |
| 98 | this.context = checkNotNull(context); |
| 99 | } |
| 100 | |
| 101 | @Override |
| 102 | public CompletableFuture<SetResponse> setConfiguration(DeviceId deviceId, |
| 103 | SetRequest request) { |
| 104 | // sanity check and handle empty change? |
| 105 | |
| 106 | // TODOs: |
| 107 | // - Construct convert request object into XML |
| 108 | // -- [FutureWork] may need to introduce behaviour for Device specific |
| 109 | // workaround insertion |
| 110 | |
| 111 | StringBuilder rpc = new StringBuilder(); |
| 112 | |
| 113 | // - Add NETCONF envelope |
Yuta HIGUCHI | 9285d97 | 2017-10-16 16:15:00 -0700 | [diff] [blame] | 114 | rpc.append("<rpc xmlns=\"").append(NETCONF_1_0_BASE_NAMESPACE).append('"') |
| 115 | .append(">"); |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 116 | |
| 117 | rpc.append("<edit-config>"); |
| 118 | rpc.append("<target>"); |
| 119 | // TODO directly writing to running for now |
| 120 | rpc.append("<running/>"); |
| 121 | rpc.append("</target>\n"); |
| 122 | rpc.append("<config ") |
| 123 | .append(XMLNS_XC).append("=\"").append(NETCONF_1_0_BASE_NAMESPACE).append("\">"); |
| 124 | // TODO netconf SBI should probably be adding these envelopes once |
| 125 | // netconf SBI is in better shape |
| 126 | // TODO In such case netconf sbi need to define namespace externally visible. |
| 127 | // ("xc" in above instance) |
| 128 | // to be used to add operations on config tree nodes |
| 129 | |
| 130 | |
| 131 | // Convert change(s) into a DataNode tree |
| 132 | for (Change change : request.changes()) { |
Yuta HIGUCHI | 9285d97 | 2017-10-16 16:15:00 -0700 | [diff] [blame] | 133 | log.trace("change={}", change); |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 134 | |
| 135 | // TODO switch statement can probably be removed |
| 136 | switch (change.op()) { |
| 137 | case REPLACE: |
| 138 | case UPDATE: |
| 139 | case DELETE: |
| 140 | // convert DataNode -> ResourceData |
| 141 | ResourceData data = toResourceData(change); |
| 142 | |
| 143 | // build CompositeData |
| 144 | DefaultCompositeData.Builder compositeData = |
| 145 | DefaultCompositeData.builder(); |
| 146 | |
| 147 | // add ResourceData |
| 148 | compositeData.resourceData(data); |
| 149 | |
| 150 | // add AnnotatedNodeInfo operation |
| 151 | compositeData.addAnnotatedNodeInfo(toAnnotatedNodeInfo(change.op(), change.path())); |
| 152 | |
| 153 | RuntimeContext yrtContext = new DefaultRuntimeContext.Builder() |
| 154 | .setDataFormat(DATAFORMAT_XML) |
| 155 | .addAnnotation(XMLNS_XC_ANNOTATION) |
| 156 | .build(); |
Yuta HIGUCHI | 9285d97 | 2017-10-16 16:15:00 -0700 | [diff] [blame] | 157 | CompositeData cdata = compositeData.build(); |
| 158 | log.trace("CompositeData:{}", cdata); |
| 159 | CompositeStream xml = context.yangRuntime().encode(cdata, |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 160 | yrtContext); |
| 161 | try { |
| 162 | CharStreams.copy(new InputStreamReader(xml.resourceData(), UTF_8), rpc); |
| 163 | } catch (IOException e) { |
| 164 | log.error("IOException thrown", e); |
| 165 | // FIXME handle error |
| 166 | } |
| 167 | break; |
| 168 | |
| 169 | default: |
| 170 | log.error("Should never reach here. {}", change); |
| 171 | break; |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | // - close NETCONF envelope |
| 176 | // TODO eventually these should be handled by NETCONF SBI side |
| 177 | rpc.append('\n'); |
| 178 | rpc.append("</config>"); |
| 179 | rpc.append("</edit-config>"); |
| 180 | rpc.append("</rpc>"); |
| 181 | |
| 182 | // - send requests down to the device |
| 183 | NetconfSession session = getNetconfSession(deviceId); |
| 184 | if (session == null) { |
| 185 | log.error("No session available for {}", deviceId); |
| 186 | return completedFuture(SetResponse.response(request, |
| 187 | Code.FAILED_PRECONDITION, |
| 188 | "No session for " + deviceId)); |
| 189 | } |
| 190 | try { |
| 191 | // FIXME Netconf async API is currently screwed up, need to fix |
| 192 | // NetconfSession, etc. |
Yuta HIGUCHI | 9285d97 | 2017-10-16 16:15:00 -0700 | [diff] [blame] | 193 | CompletableFuture<String> response = session.rpc(rpc.toString()); |
| 194 | log.trace("raw request:\n{}", rpc); |
| 195 | log.trace("prettified request:\n{}", XmlString.prettifyXml(rpc)); |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 196 | return response.handle((resp, err) -> { |
| 197 | if (err == null) { |
Yuta HIGUCHI | 9285d97 | 2017-10-16 16:15:00 -0700 | [diff] [blame] | 198 | log.trace("reply:\n{}", XmlString.prettifyXml(resp)); |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 199 | // FIXME check response properly |
| 200 | return SetResponse.ok(request); |
| 201 | } else { |
| 202 | return SetResponse.response(request, Code.UNKNOWN, err.getMessage()); |
| 203 | } |
| 204 | }); |
| 205 | } catch (NetconfException e) { |
| 206 | // TODO Handle error |
| 207 | log.error("NetconfException thrown", e); |
| 208 | return completedFuture(SetResponse.response(request, Code.UNKNOWN, e.getMessage())); |
| 209 | |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | // overridable for ease of testing |
| 214 | /** |
| 215 | * Returns a session for the specified deviceId. |
| 216 | * |
| 217 | * @param deviceId for which we wish to retrieve a session |
| 218 | * @return a NetconfSession with the specified node |
| 219 | * or null if this node does not have the session to the specified Device. |
| 220 | */ |
| 221 | protected NetconfSession getNetconfSession(DeviceId deviceId) { |
| 222 | NetconfDevice device = context.netconfController().getNetconfDevice(deviceId); |
| 223 | checkNotNull(device, "The specified deviceId could not be found by the NETCONF controller."); |
| 224 | NetconfSession session = device.getSession(); |
| 225 | checkNotNull(session, "A session could not be retrieved for the specified deviceId."); |
| 226 | return session; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Creates AnnotatedNodeInfo for {@code node}. |
| 231 | * |
| 232 | * @param op operation |
| 233 | * @param parent resourceId |
| 234 | * @param node the node |
| 235 | * @return AnnotatedNodeInfo |
| 236 | */ |
| 237 | static AnnotatedNodeInfo annotatedNodeInfo(Operation op, |
| 238 | ResourceId parent, |
| 239 | DataNode node) { |
| 240 | return DefaultAnnotatedNodeInfo.builder() |
| 241 | .resourceId(ResourceIds.resourceId(parent, node)) |
| 242 | .addAnnotation(toAnnotation(op)) |
| 243 | .build(); |
| 244 | } |
| 245 | |
| 246 | /** |
| 247 | * Creates AnnotatedNodeInfo for specified resource path. |
| 248 | * |
| 249 | * @param op operation |
| 250 | * @param path resourceId |
| 251 | * @return AnnotatedNodeInfo |
| 252 | */ |
| 253 | static AnnotatedNodeInfo toAnnotatedNodeInfo(Operation op, |
| 254 | ResourceId path) { |
| 255 | return DefaultAnnotatedNodeInfo.builder() |
| 256 | .resourceId(path) |
| 257 | .addAnnotation(toAnnotation(op)) |
| 258 | .build(); |
| 259 | } |
| 260 | |
| 261 | /** |
| 262 | * Transform DataNode into a ResourceData. |
| 263 | * |
| 264 | * @param change object |
| 265 | * @return ResourceData |
| 266 | */ |
| 267 | static ResourceData toResourceData(Change change) { |
| 268 | DefaultResourceData.Builder builder = DefaultResourceData.builder(); |
| 269 | builder.resourceId(change.path()); |
| 270 | if (change.op() != Change.Operation.DELETE) { |
| 271 | DataNode dataNode = change.val(); |
| 272 | if (dataNode instanceof InnerNode) { |
| 273 | ((InnerNode) dataNode).childNodes().values().forEach(builder::addDataNode); |
| 274 | } else { |
| 275 | log.error("Unexpected DataNode encountered", change); |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | return builder.build(); |
| 280 | } |
| 281 | |
| 282 | static Annotation toAnnotation(Operation op) { |
| 283 | switch (op) { |
| 284 | case DELETE: |
| 285 | return new DefaultAnnotation(XC_OPERATION, "remove"); |
| 286 | case REPLACE: |
| 287 | return new DefaultAnnotation(XC_OPERATION, "replace"); |
| 288 | case UPDATE: |
| 289 | return new DefaultAnnotation(XC_OPERATION, "merge"); |
| 290 | default: |
| 291 | throw new IllegalArgumentException("Unknown operation " + op); |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | } |