blob: 876ccebe3560fb022a5910f8f1b5eefd40cebf24 [file] [log] [blame]
Yuta HIGUCHI8810aa42017-08-02 15:05:37 -07001/*
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 */
16package org.onosproject.d.config.sync.impl.netconf;
17
18import static com.google.common.base.Preconditions.checkNotNull;
19import static java.nio.charset.StandardCharsets.UTF_8;
20import static java.util.concurrent.CompletableFuture.completedFuture;
21import static org.slf4j.LoggerFactory.getLogger;
22
23import java.io.IOException;
24import java.io.InputStreamReader;
25import java.util.concurrent.CompletableFuture;
26import java.util.concurrent.atomic.AtomicInteger;
27
28import org.onlab.util.XmlString;
29import org.onosproject.d.config.ResourceIds;
30import org.onosproject.d.config.sync.DeviceConfigSynchronizationProvider;
31import org.onosproject.d.config.sync.impl.netconf.NetconfDeviceConfigSynchronizerComponent.NetconfContext;
32import org.onosproject.d.config.sync.operation.SetRequest;
33import org.onosproject.d.config.sync.operation.SetRequest.Change;
34import org.onosproject.d.config.sync.operation.SetRequest.Change.Operation;
35import org.onosproject.d.config.sync.operation.SetResponse;
36import org.onosproject.d.config.sync.operation.SetResponse.Code;
37import org.onosproject.net.DeviceId;
38import org.onosproject.net.provider.AbstractProvider;
39import org.onosproject.net.provider.ProviderId;
40import org.onosproject.netconf.NetconfDevice;
41import org.onosproject.netconf.NetconfException;
42import org.onosproject.netconf.NetconfSession;
43import org.onosproject.yang.model.DataNode;
44import org.onosproject.yang.model.DefaultResourceData;
45import org.onosproject.yang.model.InnerNode;
46import org.onosproject.yang.model.ResourceData;
47import org.onosproject.yang.model.ResourceId;
48import org.onosproject.yang.runtime.AnnotatedNodeInfo;
49import org.onosproject.yang.runtime.Annotation;
50import org.onosproject.yang.runtime.CompositeStream;
51import org.onosproject.yang.runtime.DefaultAnnotatedNodeInfo;
52import org.onosproject.yang.runtime.DefaultAnnotation;
53import org.onosproject.yang.runtime.DefaultCompositeData;
54import org.onosproject.yang.runtime.DefaultRuntimeContext;
55import org.onosproject.yang.runtime.RuntimeContext;
56import org.slf4j.Logger;
57import com.google.common.io.CharStreams;
58
59/**
60 * Dynamic config synchronizer provider for NETCONF.
61 * <p>
62 * <ul>
63 * <li> Converts POJO YANG into XML.
64 * <li> Adds NETCONF envelope around it.
65 * <li> Send request down to the device over NETCONF
66 * </ul>
67 */
68public class NetconfDeviceConfigSynchronizerProvider
69 extends AbstractProvider
70 implements DeviceConfigSynchronizationProvider {
71
72 private static final Logger log = getLogger(NetconfDeviceConfigSynchronizerProvider.class);
73
74 // TODO this should probably be defined on YRT Serializer side
75 /**
76 * {@link RuntimeContext} parameter Dataformat specifying XML.
77 */
78 private static final String DATAFORMAT_XML = "xml";
79
80 private static final String XMLNS_XC = "xmlns:xc";
81 private static final String NETCONF_1_0_BASE_NAMESPACE =
82 "urn:ietf:params:xml:ns:netconf:base:1.0";
83
84 /**
85 * Annotation to add xc namespace declaration.
86 * {@value #XMLNS_XC}={@value #NETCONF_1_0_BASE_NAMESPACE}
87 */
88 private static final DefaultAnnotation XMLNS_XC_ANNOTATION =
89 new DefaultAnnotation(XMLNS_XC, NETCONF_1_0_BASE_NAMESPACE);
90
91 private static final String XC_OPERATION = "xc:operation";
92
93
94 private NetconfContext context;
95
96 // FIXME remove and let netconf southbound deal with message-id generation
97 private final AtomicInteger messageId = new AtomicInteger(1);
98
99 protected NetconfDeviceConfigSynchronizerProvider(ProviderId id,
100 NetconfContext context) {
101 super(id);
102 this.context = checkNotNull(context);
103 }
104
105 @Override
106 public CompletableFuture<SetResponse> setConfiguration(DeviceId deviceId,
107 SetRequest request) {
108 // sanity check and handle empty change?
109
110 // TODOs:
111 // - Construct convert request object into XML
112 // -- [FutureWork] may need to introduce behaviour for Device specific
113 // workaround insertion
114
115 StringBuilder rpc = new StringBuilder();
116
117 // - Add NETCONF envelope
118 rpc.append("<rpc xmlns=\"").append(NETCONF_1_0_BASE_NAMESPACE).append("\" ")
119 .append("message-id=\"").append(messageId.getAndIncrement()).append("\">");
120
121 rpc.append("<edit-config>");
122 rpc.append("<target>");
123 // TODO directly writing to running for now
124 rpc.append("<running/>");
125 rpc.append("</target>\n");
126 rpc.append("<config ")
127 .append(XMLNS_XC).append("=\"").append(NETCONF_1_0_BASE_NAMESPACE).append("\">");
128 // TODO netconf SBI should probably be adding these envelopes once
129 // netconf SBI is in better shape
130 // TODO In such case netconf sbi need to define namespace externally visible.
131 // ("xc" in above instance)
132 // to be used to add operations on config tree nodes
133
134
135 // Convert change(s) into a DataNode tree
136 for (Change change : request.changes()) {
137
138 // TODO switch statement can probably be removed
139 switch (change.op()) {
140 case REPLACE:
141 case UPDATE:
142 case DELETE:
143 // convert DataNode -> ResourceData
144 ResourceData data = toResourceData(change);
145
146 // build CompositeData
147 DefaultCompositeData.Builder compositeData =
148 DefaultCompositeData.builder();
149
150 // add ResourceData
151 compositeData.resourceData(data);
152
153 // add AnnotatedNodeInfo operation
154 compositeData.addAnnotatedNodeInfo(toAnnotatedNodeInfo(change.op(), change.path()));
155
156 RuntimeContext yrtContext = new DefaultRuntimeContext.Builder()
157 .setDataFormat(DATAFORMAT_XML)
158 .addAnnotation(XMLNS_XC_ANNOTATION)
159 .build();
160 CompositeStream xml = context.yangRuntime().encode(compositeData.build(),
161 yrtContext);
162 try {
163 CharStreams.copy(new InputStreamReader(xml.resourceData(), UTF_8), rpc);
164 } catch (IOException e) {
165 log.error("IOException thrown", e);
166 // FIXME handle error
167 }
168 break;
169
170 default:
171 log.error("Should never reach here. {}", change);
172 break;
173 }
174 }
175
176 // - close NETCONF envelope
177 // TODO eventually these should be handled by NETCONF SBI side
178 rpc.append('\n');
179 rpc.append("</config>");
180 rpc.append("</edit-config>");
181 rpc.append("</rpc>");
182
183 // - send requests down to the device
184 NetconfSession session = getNetconfSession(deviceId);
185 if (session == null) {
186 log.error("No session available for {}", deviceId);
187 return completedFuture(SetResponse.response(request,
188 Code.FAILED_PRECONDITION,
189 "No session for " + deviceId));
190 }
191 try {
192 // FIXME Netconf async API is currently screwed up, need to fix
193 // NetconfSession, etc.
194 CompletableFuture<String> response = session.request(rpc.toString());
195 log.info("TRACE: request:\n{}", XmlString.prettifyXml(rpc));
196 return response.handle((resp, err) -> {
197 if (err == null) {
198 log.info("TRACE: reply:\n{}", XmlString.prettifyXml(resp));
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}