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