blob: 344c6d0eee8c775e605279431bf12a3e237e846e [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)) {
Yuta HIGUCHI9285d972017-10-16 16:15:00 -0700168 log.trace("processing event:{}", event);
Yuta HIGUCHI8810aa42017-08-02 15:05:37 -0700169
170 DeviceId deviceId = DeviceResourceIds.toDeviceId(path);
171 ResourceId deviceRootPath = DeviceResourceIds.toResourceId(deviceId);
172
Yuta HIGUCHIe057dee2017-09-15 13:56:10 -0700173 ResourceId absPath = ResourceIds.concat(ResourceIds.ROOT_ID, path);
174 ResourceId relPath = ResourceIds.relativize(deviceRootPath, absPath);
175 // give me everything Filter
Yuta HIGUCHI8810aa42017-08-02 15:05:37 -0700176 Filter giveMeEverything = Filter.builder().build();
177
178 DataNode node = dynConfigService.readNode(path, giveMeEverything);
179 SetRequest request;
180 switch (event.type()) {
181
182 case NODE_ADDED:
183 case NODE_REPLACED:
184 request = SetRequest.builder().replace(relPath, node).build();
185 case NODE_UPDATED:
186 // Event has no pay load, only thing we can do is replace.
187 request = SetRequest.builder().replace(relPath, node).build();
188 break;
189 case NODE_DELETED:
190 request = SetRequest.builder().delete(relPath).build();
191 break;
192 case UNKNOWN_OPRN:
193 default:
194 log.error("Unexpected event {}, aborting", event);
195 return;
196 }
197
198 log.info("Dispatching {} request {}", deviceId, request);
199 CompletableFuture<SetResponse> response = dispatchRequest(deviceId, request);
200 response.whenComplete((resp, e) -> {
201 if (e == null) {
202 if (resp.code() == Code.OK) {
203 log.info("{} for {} complete", resp, deviceId);
204 } else {
205 log.warn("{} for {} had problem", resp, deviceId);
206 }
207 } else {
208 log.error("Request to {} failed {}", deviceId, response, e);
209 }
210 });
Yuta HIGUCHI9285d972017-10-16 16:15:00 -0700211 } else {
212 log.warn("Ignored event's ResourceId: {}", event.subject());
Yuta HIGUCHI8810aa42017-08-02 15:05:37 -0700213 }
214 }
215
216
217 // was sketch to handle case, where event could contain batch of things...
218 private void processEvent(DynamicConfigEvent event) {
219 // TODO assuming event object will contain batch of (atomic) change event
220
221 // What the new event will contain:
222 Type evtType = event.type();
223
224 // Transaction ID, can be null
225 TransactionId txId = null;
226
227 // TODO this might change into collection of (path, val_before, val_after)
228
229 ResourceId path = event.subject();
230 // data node (can be tree) representing change, it could be incremental update
231 DataNode val = null;
232
233 // build per-Device SetRequest
234 // val could be a tree, containing multiple Device tree,
235 // break them down into per-Device sub-tree
236 Map<DeviceId, SetRequest.Builder> requests = new HashMap<>();
237
238 if (isUnderDeviceRootNode(path)) {
239 // about single device
240 buildDeviceRequest(requests, evtType, path, toDeviceId(path), val);
241
242 } else if (DeviceResourceIds.isRootOrDevicesNode(path)) {
243 // => potentially contain changes spanning multiple Devices
244 Map<DeviceId, DataNode> perDevices = perDevices(path, val);
245
246 perDevices.forEach((did, dataNode) -> {
247 buildDeviceRequest(requests, evtType, toResourceId(did), did, dataNode);
248 });
249
250 // TODO special care is probably required for delete cases
251 // especially delete all under devices
252
253 } else {
254 log.warn("Event not related to a Device?");
255 }
256
257
258 // TODO assuming event is a batch,
259 // potentially containing changes for multiple devices,
260 // who will process/coordinate the batch event?
261
262
263 // TODO loop through per-Device change set
264 List<CompletableFuture<SetResponse>> responses =
265 requests.entrySet().stream()
266 .map(entry -> dispatchRequest(entry.getKey(), entry.getValue().build()))
267 .collect(Collectors.toList());
268
269 // wait for all responses
270 List<SetResponse> allResults = Tools.allOf(responses).join();
271 // TODO deal with partial failure case (multi-device coordination)
272 log.info("DEBUG: results: {}", allResults);
273 }
274
275 // might make sense to make this public
276 CompletableFuture<SetResponse> dispatchRequest(DeviceId devId, SetRequest req) {
277
278 // determine appropriate provider for this Device
279 DeviceConfigSynchronizationProvider provider = this.getProvider(devId);
280
281 if (provider == null) {
282 // no appropriate provider found
283 // return completed future with failed SetResponse
284 return completedFuture(response(req,
285 SetResponse.Code.FAILED_PRECONDITION,
286 "no provider found for " + devId));
287 }
288
289 // dispatch request
290 return provider.setConfiguration(devId, req)
291 .handle((resp, err) -> {
292 if (err == null) {
293 // set complete
294 log.info("DEBUG: Req:{}, Resp:{}", req, resp);
295 return resp;
296 } else {
297 // fatal error
298 log.error("Fatal error on {}", req, err);
299 return response(req,
300 SetResponse.Code.UNKNOWN,
301 "Unknown error " + err);
302 }
303 });
304 }
305
306
307 // may eventually reuse with batch event
308 /**
309 * Build device request about a Device.
310 *
311 * @param requests map containing request builder to populate
312 * @param evtType change request type
313 * @param path to {@code val}
314 * @param did DeviceId which {@code path} is about
315 * @param val changed node to write
316 */
317 private void buildDeviceRequest(Map<DeviceId, SetRequest.Builder> requests,
318 Type evtType,
319 ResourceId path,
320 DeviceId did,
321 DataNode val) {
322
323 SetRequest.Builder request =
324 requests.computeIfAbsent(did, d -> SetRequest.builder());
325
326 switch (evtType) {
327 case NODE_ADDED:
328 case NODE_REPLACED:
329 request.replace(path, val);
330 break;
331
332 case NODE_UPDATED:
333 // TODO Auto-generated method stub
334 request.update(path, val);
335 break;
336
337 case NODE_DELETED:
338 // TODO Auto-generated method stub
339 request.delete(path);
340 break;
341
342 case UNKNOWN_OPRN:
343 default:
344 log.warn("Ignoring unexpected {}", evtType);
345 break;
346 }
347 }
348
349 /**
350 * Breaks down tree {@code val} into per Device subtree.
351 *
352 * @param path pointing to {@code val}
353 * @param val tree which contains only 1 Device.
354 * @return Device node relative DataNode for each DeviceId
355 * @throws IllegalArgumentException
356 */
357 private static Map<DeviceId, DataNode> perDevices(ResourceId path, DataNode val) {
358 if (DeviceResourceIds.isUnderDeviceRootNode(path)) {
359 // - if path is device root or it's subtree, path alone is sufficient
360 return ImmutableMap.of(DeviceResourceIds.toDeviceId(path), val);
361
362 } else if (DeviceResourceIds.isRootOrDevicesNode(path)) {
363 // - if path is "/" or devices, it might be constructible from val tree
364 final Collection<DataNode> devicesChildren;
365 if (DeviceResourceIds.isRootNode(path)) {
366 // root
367 devicesChildren = DataNodes.childOnlyByName(val, DeviceResourceIds.DEVICES_NAME)
368 .map(dn -> DataNodes.children(dn))
369 .orElse(ImmutableList.of());
370 } else {
371 // devices
372 devicesChildren = DataNodes.children(val);
373 }
374
375 return devicesChildren.stream()
376 // TODO use full schemaId for filtering when ready
377 .filter(dn -> dn.key().schemaId().name().equals(DeviceResourceIds.DEVICE_NAME))
378 .collect(Collectors.toMap(dn -> DeviceResourceIds.toDeviceId(dn.key()),
379 dn -> dn));
380
381 }
382 throw new IllegalArgumentException(path + " not related to Device");
383 }
384
385}