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