Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 1 | /* |
| 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 | */ |
| 16 | package org.onosproject.d.config.sync.impl; |
| 17 | |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 18 | import com.google.common.annotations.Beta; |
| 19 | import com.google.common.collect.ImmutableList; |
| 20 | import com.google.common.collect.ImmutableMap; |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 21 | import org.onlab.util.Tools; |
| 22 | import org.onosproject.config.DynamicConfigEvent; |
| 23 | import org.onosproject.config.DynamicConfigEvent.Type; |
| 24 | import org.onosproject.config.DynamicConfigListener; |
| 25 | import org.onosproject.config.DynamicConfigService; |
| 26 | import org.onosproject.config.Filter; |
| 27 | import org.onosproject.d.config.DataNodes; |
| 28 | import org.onosproject.d.config.DeviceResourceIds; |
| 29 | import org.onosproject.d.config.ResourceIds; |
| 30 | import org.onosproject.d.config.sync.DeviceConfigSynchronizationProvider; |
| 31 | import org.onosproject.d.config.sync.DeviceConfigSynchronizationProviderRegistry; |
| 32 | import org.onosproject.d.config.sync.DeviceConfigSynchronizationProviderService; |
| 33 | import org.onosproject.d.config.sync.operation.SetRequest; |
| 34 | import org.onosproject.d.config.sync.operation.SetResponse; |
| 35 | import org.onosproject.d.config.sync.operation.SetResponse.Code; |
| 36 | import org.onosproject.net.DeviceId; |
| 37 | import org.onosproject.net.config.NetworkConfigService; |
| 38 | import org.onosproject.net.provider.AbstractProviderRegistry; |
| 39 | import org.onosproject.net.provider.AbstractProviderService; |
| 40 | import org.onosproject.store.primitives.TransactionId; |
| 41 | import org.onosproject.yang.model.DataNode; |
| 42 | import org.onosproject.yang.model.ResourceId; |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 43 | import org.osgi.service.component.annotations.Activate; |
| 44 | import org.osgi.service.component.annotations.Component; |
| 45 | import org.osgi.service.component.annotations.Deactivate; |
| 46 | import org.osgi.service.component.annotations.Reference; |
| 47 | import org.osgi.service.component.annotations.ReferenceCardinality; |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 48 | import org.slf4j.Logger; |
| 49 | |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 50 | import java.time.Duration; |
| 51 | import java.util.Collection; |
| 52 | import java.util.HashMap; |
| 53 | import java.util.List; |
| 54 | import java.util.Map; |
| 55 | import java.util.concurrent.CompletableFuture; |
| 56 | import java.util.stream.Collectors; |
| 57 | |
| 58 | import static java.util.concurrent.CompletableFuture.completedFuture; |
| 59 | import static org.onosproject.d.config.DeviceResourceIds.isUnderDeviceRootNode; |
| 60 | import static org.onosproject.d.config.DeviceResourceIds.toDeviceId; |
| 61 | import static org.onosproject.d.config.DeviceResourceIds.toResourceId; |
| 62 | import static org.onosproject.d.config.sync.operation.SetResponse.response; |
| 63 | import static org.slf4j.LoggerFactory.getLogger; |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 64 | |
| 65 | /** |
| 66 | * Component to bridge Dynamic Config store and the Device configuration state. |
Yuta HIGUCHI | ab35080 | 2018-05-07 14:56:32 -0700 | [diff] [blame] | 67 | * |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 68 | * <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 Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 74 | @Component(immediate = true, service = DeviceConfigSynchronizationProviderRegistry.class) |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 75 | public class DynamicDeviceConfigSynchronizer |
| 76 | extends AbstractProviderRegistry<DeviceConfigSynchronizationProvider, |
| 77 | DeviceConfigSynchronizationProviderService> |
| 78 | implements DeviceConfigSynchronizationProviderRegistry { |
| 79 | |
| 80 | private static final Logger log = getLogger(DynamicDeviceConfigSynchronizer.class); |
| 81 | |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 82 | @Reference(cardinality = ReferenceCardinality.MANDATORY) |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 83 | protected DynamicConfigService dynConfigService; |
| 84 | |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 85 | @Reference(cardinality = ReferenceCardinality.MANDATORY) |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 86 | protected NetworkConfigService netcfgService; |
| 87 | |
| 88 | private DynamicConfigListener listener = new InnerDyConListener(); |
| 89 | |
Yuta HIGUCHI | 110ba89 | 2018-01-30 13:47:55 -0800 | [diff] [blame] | 90 | // FIXME hack for unconsolidated event bug |
| 91 | private Duration quietPeriod = Duration.ofSeconds(2); |
| 92 | private long quietUntil = 0; |
| 93 | |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 94 | @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 HIGUCHI | 110ba89 | 2018-01-30 13:47:55 -0800 | [diff] [blame] | 168 | if (System.currentTimeMillis() < quietUntil) { |
| 169 | log.trace("Ignoring {}. Quiet period until {}", |
| 170 | event, Tools.defaultOffsetDataTime(quietUntil)); |
| 171 | return; |
| 172 | } |
| 173 | |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 174 | ResourceId path = event.subject(); |
| 175 | if (isUnderDeviceRootNode(path)) { |
Yuta HIGUCHI | 9285d97 | 2017-10-16 16:15:00 -0700 | [diff] [blame] | 176 | log.trace("processing event:{}", event); |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 177 | |
| 178 | DeviceId deviceId = DeviceResourceIds.toDeviceId(path); |
| 179 | ResourceId deviceRootPath = DeviceResourceIds.toResourceId(deviceId); |
| 180 | |
Yuta HIGUCHI | e057dee | 2017-09-15 13:56:10 -0700 | [diff] [blame] | 181 | ResourceId absPath = ResourceIds.concat(ResourceIds.ROOT_ID, path); |
| 182 | ResourceId relPath = ResourceIds.relativize(deviceRootPath, absPath); |
| 183 | // give me everything Filter |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 184 | 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 HIGUCHI | 6800ced | 2017-11-20 11:15:37 -0800 | [diff] [blame] | 193 | break; |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 194 | 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 HIGUCHI | 110ba89 | 2018-01-30 13:47:55 -0800 | [diff] [blame] | 220 | |
| 221 | // FIXME hack for unconsolidated event bug |
| 222 | quietUntil = System.currentTimeMillis() + quietPeriod.toMillis(); |
Yuta HIGUCHI | 9285d97 | 2017-10-16 16:15:00 -0700 | [diff] [blame] | 223 | } else { |
Andrea Campanella | fa8bf1f | 2018-11-26 11:57:45 -0800 | [diff] [blame] | 224 | log.debug("Ignored event's ResourceId: {}", event.subject()); |
Yuta HIGUCHI | 8810aa4 | 2017-08-02 15:05:37 -0700 | [diff] [blame] | 225 | } |
| 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 | } |