blob: f9fdfb730fb7dcabd641883632ec309f4bcb0f42 [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;
17
18import static java.util.concurrent.CompletableFuture.completedFuture;
19import static org.onosproject.d.config.DeviceResourceIds.isUnderDeviceRootNode;
20import static org.onosproject.d.config.DeviceResourceIds.toDeviceId;
21import static org.onosproject.d.config.DeviceResourceIds.toResourceId;
22import static org.onosproject.d.config.sync.operation.SetResponse.response;
23import static org.slf4j.LoggerFactory.getLogger;
24
25import java.util.Collection;
26import java.util.HashMap;
27import java.util.List;
28import java.util.Map;
29import java.util.concurrent.CompletableFuture;
30import java.util.stream.Collectors;
31
32import org.apache.felix.scr.annotations.Activate;
33import org.apache.felix.scr.annotations.Component;
34import org.apache.felix.scr.annotations.Deactivate;
35import org.apache.felix.scr.annotations.Reference;
36import org.apache.felix.scr.annotations.ReferenceCardinality;
37import org.apache.felix.scr.annotations.Service;
38import org.onlab.util.Tools;
39import org.onosproject.config.DynamicConfigEvent;
40import org.onosproject.config.DynamicConfigEvent.Type;
41import org.onosproject.config.DynamicConfigListener;
42import org.onosproject.config.DynamicConfigService;
43import org.onosproject.config.Filter;
44import org.onosproject.d.config.DataNodes;
45import org.onosproject.d.config.DeviceResourceIds;
46import org.onosproject.d.config.ResourceIds;
47import org.onosproject.d.config.sync.DeviceConfigSynchronizationProvider;
48import org.onosproject.d.config.sync.DeviceConfigSynchronizationProviderRegistry;
49import org.onosproject.d.config.sync.DeviceConfigSynchronizationProviderService;
50import org.onosproject.d.config.sync.operation.SetRequest;
51import org.onosproject.d.config.sync.operation.SetResponse;
52import org.onosproject.d.config.sync.operation.SetResponse.Code;
53import org.onosproject.net.DeviceId;
54import org.onosproject.net.config.NetworkConfigService;
55import org.onosproject.net.provider.AbstractProviderRegistry;
56import org.onosproject.net.provider.AbstractProviderService;
57import org.onosproject.store.primitives.TransactionId;
58import org.onosproject.yang.model.DataNode;
59import org.onosproject.yang.model.ResourceId;
60import org.slf4j.Logger;
61
62import com.google.common.annotations.Beta;
63import com.google.common.collect.ImmutableList;
64import com.google.common.collect.ImmutableMap;
65
66/**
67 * Component to bridge Dynamic Config store and the Device configuration state.
68 * <p>
69 * <ul>
70 * <li> Propagate DynamicConfig service change downward to Device side via provider.
71 * <li> Propagate Device triggered change event upward to DyamicConfig service.
72 * </ul>
73 */
74@Beta
75@Component(immediate = true)
76@Service
77public class DynamicDeviceConfigSynchronizer
78 extends AbstractProviderRegistry<DeviceConfigSynchronizationProvider,
79 DeviceConfigSynchronizationProviderService>
80 implements DeviceConfigSynchronizationProviderRegistry {
81
82 private static final Logger log = getLogger(DynamicDeviceConfigSynchronizer.class);
83
84 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
85 protected DynamicConfigService dynConfigService;
86
87 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
88 protected NetworkConfigService netcfgService;
89
90 private DynamicConfigListener listener = new InnerDyConListener();
91
92 @Activate
93 public void activate() {
94 // TODO start background task to sync Controller and Device?
95 dynConfigService.addListener(listener);
96 log.info("Started");
97 }
98
99 @Deactivate
100 public void deactivate() {
101 dynConfigService.removeListener(listener);
102 log.info("Stopped");
103 }
104
105
106 @Override
107 protected DeviceConfigSynchronizationProviderService createProviderService(
108 DeviceConfigSynchronizationProvider provider) {
109 return new InternalConfigSynchronizationServiceProvider(provider);
110 }
111
112 @Override
113 protected DeviceConfigSynchronizationProvider defaultProvider() {
114 // TODO return provider instance which can deal with "general" provider?
115 return super.defaultProvider();
116 }
117
118 /**
119 * Proxy to relay Device change event for propagating running "state"
120 * information up to dynamic configuration service.
121 */
122 class InternalConfigSynchronizationServiceProvider
123 extends AbstractProviderService<DeviceConfigSynchronizationProvider>
124 implements DeviceConfigSynchronizationProviderService {
125
126 protected InternalConfigSynchronizationServiceProvider(DeviceConfigSynchronizationProvider provider) {
127 super(provider);
128 }
129
130 // TODO API for passive information propagation to be added later on
131 }
132
133 /**
134 * DynamicConfigListener to trigger active synchronization toward the device.
135 */
136 class InnerDyConListener implements DynamicConfigListener {
137
138 @Override
139 public boolean isRelevant(DynamicConfigEvent event) {
140 // TODO NetconfActiveComponent.isRelevant(DynamicConfigEvent)
141 // seems to be doing some filtering
142 // Logic filtering for L3VPN is probably a demo hack,
143 // but is there any portion of it which is really needed?
144 // e.g., listen only for device tree events?
145
146 ResourceId path = event.subject();
147 // TODO only device tree related event is relevant.
148 // 1) path is under device tree
149 // 2) path is root, and DataNode contains element under node
150 // ...
151 return true;
152 }
153
154 @Override
155 public void event(DynamicConfigEvent event) {
156 // Note: removed accumulator in the old code assuming,
157 // event accumulation will happen on Device Config Event level.
158
159 // TODO execute off event dispatch thread
160 processEventNonBatch(event);
161 }
162
163 }
164
165 void processEventNonBatch(DynamicConfigEvent event) {
166 ResourceId path = event.subject();
167 if (isUnderDeviceRootNode(path)) {
168
169 DeviceId deviceId = DeviceResourceIds.toDeviceId(path);
170 ResourceId deviceRootPath = DeviceResourceIds.toResourceId(deviceId);
171
Yuta HIGUCHIe057dee2017-09-15 13:56:10 -0700172 ResourceId absPath = ResourceIds.concat(ResourceIds.ROOT_ID, path);
173 ResourceId relPath = ResourceIds.relativize(deviceRootPath, absPath);
174 // give me everything Filter
Yuta HIGUCHI8810aa42017-08-02 15:05:37 -0700175 Filter giveMeEverything = Filter.builder().build();
176
177 DataNode node = dynConfigService.readNode(path, giveMeEverything);
178 SetRequest request;
179 switch (event.type()) {
180
181 case NODE_ADDED:
182 case NODE_REPLACED:
183 request = SetRequest.builder().replace(relPath, node).build();
184 case NODE_UPDATED:
185 // Event has no pay load, only thing we can do is replace.
186 request = SetRequest.builder().replace(relPath, node).build();
187 break;
188 case NODE_DELETED:
189 request = SetRequest.builder().delete(relPath).build();
190 break;
191 case UNKNOWN_OPRN:
192 default:
193 log.error("Unexpected event {}, aborting", event);
194 return;
195 }
196
197 log.info("Dispatching {} request {}", deviceId, request);
198 CompletableFuture<SetResponse> response = dispatchRequest(deviceId, request);
199 response.whenComplete((resp, e) -> {
200 if (e == null) {
201 if (resp.code() == Code.OK) {
202 log.info("{} for {} complete", resp, deviceId);
203 } else {
204 log.warn("{} for {} had problem", resp, deviceId);
205 }
206 } else {
207 log.error("Request to {} failed {}", deviceId, response, e);
208 }
209 });
210 }
211 }
212
213
214 // was sketch to handle case, where event could contain batch of things...
215 private void processEvent(DynamicConfigEvent event) {
216 // TODO assuming event object will contain batch of (atomic) change event
217
218 // What the new event will contain:
219 Type evtType = event.type();
220
221 // Transaction ID, can be null
222 TransactionId txId = null;
223
224 // TODO this might change into collection of (path, val_before, val_after)
225
226 ResourceId path = event.subject();
227 // data node (can be tree) representing change, it could be incremental update
228 DataNode val = null;
229
230 // build per-Device SetRequest
231 // val could be a tree, containing multiple Device tree,
232 // break them down into per-Device sub-tree
233 Map<DeviceId, SetRequest.Builder> requests = new HashMap<>();
234
235 if (isUnderDeviceRootNode(path)) {
236 // about single device
237 buildDeviceRequest(requests, evtType, path, toDeviceId(path), val);
238
239 } else if (DeviceResourceIds.isRootOrDevicesNode(path)) {
240 // => potentially contain changes spanning multiple Devices
241 Map<DeviceId, DataNode> perDevices = perDevices(path, val);
242
243 perDevices.forEach((did, dataNode) -> {
244 buildDeviceRequest(requests, evtType, toResourceId(did), did, dataNode);
245 });
246
247 // TODO special care is probably required for delete cases
248 // especially delete all under devices
249
250 } else {
251 log.warn("Event not related to a Device?");
252 }
253
254
255 // TODO assuming event is a batch,
256 // potentially containing changes for multiple devices,
257 // who will process/coordinate the batch event?
258
259
260 // TODO loop through per-Device change set
261 List<CompletableFuture<SetResponse>> responses =
262 requests.entrySet().stream()
263 .map(entry -> dispatchRequest(entry.getKey(), entry.getValue().build()))
264 .collect(Collectors.toList());
265
266 // wait for all responses
267 List<SetResponse> allResults = Tools.allOf(responses).join();
268 // TODO deal with partial failure case (multi-device coordination)
269 log.info("DEBUG: results: {}", allResults);
270 }
271
272 // might make sense to make this public
273 CompletableFuture<SetResponse> dispatchRequest(DeviceId devId, SetRequest req) {
274
275 // determine appropriate provider for this Device
276 DeviceConfigSynchronizationProvider provider = this.getProvider(devId);
277
278 if (provider == null) {
279 // no appropriate provider found
280 // return completed future with failed SetResponse
281 return completedFuture(response(req,
282 SetResponse.Code.FAILED_PRECONDITION,
283 "no provider found for " + devId));
284 }
285
286 // dispatch request
287 return provider.setConfiguration(devId, req)
288 .handle((resp, err) -> {
289 if (err == null) {
290 // set complete
291 log.info("DEBUG: Req:{}, Resp:{}", req, resp);
292 return resp;
293 } else {
294 // fatal error
295 log.error("Fatal error on {}", req, err);
296 return response(req,
297 SetResponse.Code.UNKNOWN,
298 "Unknown error " + err);
299 }
300 });
301 }
302
303
304 // may eventually reuse with batch event
305 /**
306 * Build device request about a Device.
307 *
308 * @param requests map containing request builder to populate
309 * @param evtType change request type
310 * @param path to {@code val}
311 * @param did DeviceId which {@code path} is about
312 * @param val changed node to write
313 */
314 private void buildDeviceRequest(Map<DeviceId, SetRequest.Builder> requests,
315 Type evtType,
316 ResourceId path,
317 DeviceId did,
318 DataNode val) {
319
320 SetRequest.Builder request =
321 requests.computeIfAbsent(did, d -> SetRequest.builder());
322
323 switch (evtType) {
324 case NODE_ADDED:
325 case NODE_REPLACED:
326 request.replace(path, val);
327 break;
328
329 case NODE_UPDATED:
330 // TODO Auto-generated method stub
331 request.update(path, val);
332 break;
333
334 case NODE_DELETED:
335 // TODO Auto-generated method stub
336 request.delete(path);
337 break;
338
339 case UNKNOWN_OPRN:
340 default:
341 log.warn("Ignoring unexpected {}", evtType);
342 break;
343 }
344 }
345
346 /**
347 * Breaks down tree {@code val} into per Device subtree.
348 *
349 * @param path pointing to {@code val}
350 * @param val tree which contains only 1 Device.
351 * @return Device node relative DataNode for each DeviceId
352 * @throws IllegalArgumentException
353 */
354 private static Map<DeviceId, DataNode> perDevices(ResourceId path, DataNode val) {
355 if (DeviceResourceIds.isUnderDeviceRootNode(path)) {
356 // - if path is device root or it's subtree, path alone is sufficient
357 return ImmutableMap.of(DeviceResourceIds.toDeviceId(path), val);
358
359 } else if (DeviceResourceIds.isRootOrDevicesNode(path)) {
360 // - if path is "/" or devices, it might be constructible from val tree
361 final Collection<DataNode> devicesChildren;
362 if (DeviceResourceIds.isRootNode(path)) {
363 // root
364 devicesChildren = DataNodes.childOnlyByName(val, DeviceResourceIds.DEVICES_NAME)
365 .map(dn -> DataNodes.children(dn))
366 .orElse(ImmutableList.of());
367 } else {
368 // devices
369 devicesChildren = DataNodes.children(val);
370 }
371
372 return devicesChildren.stream()
373 // TODO use full schemaId for filtering when ready
374 .filter(dn -> dn.key().schemaId().name().equals(DeviceResourceIds.DEVICE_NAME))
375 .collect(Collectors.toMap(dn -> DeviceResourceIds.toDeviceId(dn.key()),
376 dn -> dn));
377
378 }
379 throw new IllegalArgumentException(path + " not related to Device");
380 }
381
382}