blob: f36e7a85ea5cb0bb869b3d8dbc45205611d603eb [file] [log] [blame]
Yi Tsenge616d752018-11-27 10:53:27 -08001/*
2 * Copyright 2018-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
17package org.onosproject.provider.general.device.impl;
18
19import com.google.common.annotations.Beta;
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080020import com.google.common.collect.Maps;
Yi Tsenge616d752018-11-27 10:53:27 -080021import com.google.common.util.concurrent.Striped;
22import gnmi.Gnmi.Notification;
23import gnmi.Gnmi.Path;
24import gnmi.Gnmi.PathElem;
25import gnmi.Gnmi.SubscribeRequest;
26import gnmi.Gnmi.Subscription;
27import gnmi.Gnmi.SubscriptionList;
28import gnmi.Gnmi.SubscriptionMode;
29import gnmi.Gnmi.Update;
Yi Tsenge616d752018-11-27 10:53:27 -080030import org.onosproject.gnmi.api.GnmiController;
31import org.onosproject.gnmi.api.GnmiEvent;
32import org.onosproject.gnmi.api.GnmiEventListener;
33import org.onosproject.gnmi.api.GnmiUpdate;
34import org.onosproject.gnmi.api.GnmiUtils;
35import org.onosproject.mastership.MastershipEvent;
36import org.onosproject.mastership.MastershipListener;
37import org.onosproject.mastership.MastershipService;
Yi Tseng59d5f3e2018-11-27 23:09:41 -080038import org.onosproject.net.DefaultAnnotations;
Yi Tsenge616d752018-11-27 10:53:27 -080039import org.onosproject.net.DeviceId;
40import org.onosproject.net.Port;
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080041import org.onosproject.net.PortNumber;
Yi Tsenge616d752018-11-27 10:53:27 -080042import org.onosproject.net.device.DefaultPortDescription;
43import org.onosproject.net.device.DeviceEvent;
44import org.onosproject.net.device.DeviceListener;
45import org.onosproject.net.device.DeviceProviderService;
46import org.onosproject.net.device.DeviceService;
47import org.onosproject.net.device.PortDescription;
48import org.slf4j.Logger;
49import org.slf4j.LoggerFactory;
50
Yi Tsenge616d752018-11-27 10:53:27 -080051import java.util.List;
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080052import java.util.Map;
53import java.util.Objects;
54import java.util.Set;
pierventre52ef9332021-07-09 22:42:17 +020055import java.util.concurrent.ExecutorService;
Yi Tsenge616d752018-11-27 10:53:27 -080056import java.util.concurrent.locks.Lock;
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080057import java.util.stream.Collectors;
Yi Tsenge616d752018-11-27 10:53:27 -080058
pierventre52ef9332021-07-09 22:42:17 +020059import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
60import static org.onlab.util.Tools.groupedThreads;
61
Yi Tsenge616d752018-11-27 10:53:27 -080062/**
63 * Entity that manages gNMI subscription for devices using OpenConfig models and
64 * that reports relevant events to the core.
65 */
66@Beta
67class GnmiDeviceStateSubscriber {
68
pierventre50696a72021-06-01 12:26:36 +020069 private static final String LAST_CHANGE = "last-change";
Yi Tseng59d5f3e2018-11-27 23:09:41 -080070
Yi Tsenge616d752018-11-27 10:53:27 -080071 private static Logger log = LoggerFactory.getLogger(GnmiDeviceStateSubscriber.class);
72
73 private final GnmiController gnmiController;
74 private final DeviceService deviceService;
75 private final DeviceProviderService providerService;
76 private final MastershipService mastershipService;
77
Yi Tsenge616d752018-11-27 10:53:27 -080078 private final InternalGnmiEventListener gnmiEventListener = new InternalGnmiEventListener();
79 private final InternalDeviceListener deviceEventListener = new InternalDeviceListener();
80 private final InternalMastershipListener mastershipListener = new InternalMastershipListener();
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080081 private final Map<DeviceId, Set<PortNumber>> deviceSubscribed = Maps.newHashMap();
Yi Tsenge616d752018-11-27 10:53:27 -080082
83 private final Striped<Lock> deviceLocks = Striped.lock(30);
84
pierventre52ef9332021-07-09 22:42:17 +020085 private ExecutorService eventExecutor;
86
Yi Tsenge616d752018-11-27 10:53:27 -080087 GnmiDeviceStateSubscriber(GnmiController gnmiController, DeviceService deviceService,
88 MastershipService mastershipService,
89 DeviceProviderService providerService) {
90 this.gnmiController = gnmiController;
91 this.deviceService = deviceService;
92 this.mastershipService = mastershipService;
93 this.providerService = providerService;
94 }
95
96 public void activate() {
pierventre52ef9332021-07-09 22:42:17 +020097 eventExecutor = newSingleThreadScheduledExecutor(groupedThreads(
98 "onos/gnmi", "events-%d", log));
Yi Tsenge616d752018-11-27 10:53:27 -080099 deviceService.addListener(deviceEventListener);
100 mastershipService.addListener(mastershipListener);
101 gnmiController.addListener(gnmiEventListener);
102 // Subscribe to existing devices.
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800103 deviceService.getDevices().forEach(d -> checkSubscription(d.id()));
Yi Tsenge616d752018-11-27 10:53:27 -0800104 }
105
106 public void deactivate() {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800107 deviceSubscribed.keySet().forEach(this::unsubscribeIfNeeded);
Yi Tsenge616d752018-11-27 10:53:27 -0800108 deviceService.removeListener(deviceEventListener);
109 mastershipService.removeListener(mastershipListener);
110 gnmiController.removeListener(gnmiEventListener);
pierventre52ef9332021-07-09 22:42:17 +0200111 eventExecutor.shutdownNow();
112 eventExecutor = null;
Yi Tsenge616d752018-11-27 10:53:27 -0800113 }
114
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800115 private void checkSubscription(DeviceId deviceId) {
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700116 if (gnmiController.get(deviceId) == null) {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800117 // Ignore devices for which a gNMI client does not exist.
118 return;
119 }
Yi Tsenge616d752018-11-27 10:53:27 -0800120 deviceLocks.get(deviceId).lock();
121 try {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800122 if (shouldHaveSubscription(deviceId)) {
Yi Tsenge616d752018-11-27 10:53:27 -0800123 subscribeIfNeeded(deviceId);
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800124 } else {
125 unsubscribeIfNeeded(deviceId);
Yi Tsenge616d752018-11-27 10:53:27 -0800126 }
127 } finally {
128 deviceLocks.get(deviceId).unlock();
129 }
130 }
131
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800132 private boolean shouldHaveSubscription(DeviceId deviceId) {
133 return deviceService.getDevice(deviceId) != null
134 && deviceService.isAvailable(deviceId)
135 && mastershipService.isLocalMaster(deviceId)
136 && !deviceService.getPorts(deviceId).isEmpty();
137 }
138
pierventre50696a72021-06-01 12:26:36 +0200139 private Path interfaceStatePath(String interfaceName) {
Yi Tsenge616d752018-11-27 10:53:27 -0800140 return Path.newBuilder()
141 .addElem(PathElem.newBuilder().setName("interfaces").build())
142 .addElem(PathElem.newBuilder()
143 .setName("interface").putKey("name", interfaceName).build())
144 .addElem(PathElem.newBuilder().setName("state").build())
Yi Tsenge616d752018-11-27 10:53:27 -0800145 .build();
146 }
147
148 private void unsubscribeIfNeeded(DeviceId deviceId) {
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700149 gnmiController.get(deviceId).unsubscribe();
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800150 if (deviceSubscribed.remove(deviceId) != null) {
151 log.info("Cancelled gNMI subscription for {}", deviceId);
Yi Tsenge616d752018-11-27 10:53:27 -0800152 }
Yi Tsenge616d752018-11-27 10:53:27 -0800153 }
154
155 private void subscribeIfNeeded(DeviceId deviceId) {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800156
157 Set<PortNumber> ports = deviceService.getPorts(deviceId).stream()
158 .map(Port::number)
159 .collect(Collectors.toSet());
160
161 if (Objects.equals(ports, deviceSubscribed.get(deviceId))) {
162 // Already subscribed for the same ports.
Yi Tsenge616d752018-11-27 10:53:27 -0800163 return;
164 }
165
Carmelo Cascone67a317d2019-03-18 17:08:22 -0700166 // Subscribe for the new set of ports.
167 deviceSubscribed.put(deviceId, ports);
Yi Tsenge616d752018-11-27 10:53:27 -0800168
Carmelo Cascone67a317d2019-03-18 17:08:22 -0700169 // Send subscription request.
170 final SubscriptionList subscriptionList = SubscriptionList.newBuilder()
171 .setMode(SubscriptionList.Mode.STREAM)
172 .setUpdatesOnly(true)
173 .addAllSubscription(ports.stream().map(
174 port -> Subscription.newBuilder()
pierventre50696a72021-06-01 12:26:36 +0200175 .setPath(interfaceStatePath(port.name()))
Carmelo Cascone67a317d2019-03-18 17:08:22 -0700176 .setMode(SubscriptionMode.ON_CHANGE)
177 .build()).collect(Collectors.toList()))
Yi Tsenge616d752018-11-27 10:53:27 -0800178 .build();
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700179 gnmiController.get(deviceId).subscribe(
Carmelo Cascone67a317d2019-03-18 17:08:22 -0700180 SubscribeRequest.newBuilder()
181 .setSubscribe(subscriptionList)
182 .build());
Yi Tsenge616d752018-11-27 10:53:27 -0800183
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800184 log.info("Started gNMI subscription for {} ports on {}", ports.size(), deviceId);
Yi Tsenge616d752018-11-27 10:53:27 -0800185 }
186
187 private void handleGnmiUpdate(GnmiUpdate eventSubject) {
188 Notification notification = eventSubject.update();
189 if (notification == null) {
190 log.warn("Cannot handle gNMI event without update data, abort");
191 log.debug("gNMI update:\n{}", eventSubject);
192 return;
193 }
194
pierventre50696a72021-06-01 12:26:36 +0200195 long lastChange = 0;
196 Update statusUpdate = null;
197 Path path;
198 PathElem lastElem;
199 // The assumption is that the notification contains all the updates:
200 // last-change, oper-status, counters, and so on. Otherwise, we need
201 // to put in place the aggregation logic in ONOS
202 for (Update update : notification.getUpdateList()) {
203 path = update.getPath();
204 lastElem = path.getElem(path.getElemCount() - 1);
Yi Tsenge616d752018-11-27 10:53:27 -0800205
206 // Use last element to identify which state updated
207 if ("oper-status".equals(lastElem.getName())) {
pierventre50696a72021-06-01 12:26:36 +0200208 statusUpdate = update;
209 } else if ("last-change".equals(lastElem.getName())) {
210 lastChange = update.getVal().getUintVal();
211 } else if (log.isDebugEnabled()) {
Yi Tsenge616d752018-11-27 10:53:27 -0800212 log.debug("Unrecognized update {}", GnmiUtils.pathToString(path));
213 }
pierventre50696a72021-06-01 12:26:36 +0200214 }
215
216 // Last-change could be not supported by the device
217 // Cannot proceed without the status update.
218 if (statusUpdate != null) {
219 handleOperStatusUpdate(eventSubject.deviceId(), statusUpdate, lastChange);
220 }
Yi Tsenge616d752018-11-27 10:53:27 -0800221 }
222
Yi Tseng59d5f3e2018-11-27 23:09:41 -0800223 private void handleOperStatusUpdate(DeviceId deviceId, Update update, long timestamp) {
Yi Tsenge616d752018-11-27 10:53:27 -0800224 Path path = update.getPath();
225 // first element should be "interface"
226 String interfaceName = path.getElem(1).getKeyOrDefault("name", null);
227 if (interfaceName == null) {
228 log.error("No interface present in gNMI update, abort");
229 log.debug("gNMI update:\n{}", update);
230 return;
231 }
232
233 List<Port> portsFromDevice = deviceService.getPorts(deviceId);
234 portsFromDevice.forEach(port -> {
235 if (!port.number().name().equals(interfaceName)) {
236 return;
237 }
238
Yi Tseng59d5f3e2018-11-27 23:09:41 -0800239 DefaultAnnotations portAnnotations = DefaultAnnotations.builder()
240 .putAll(port.annotations())
241 .set(LAST_CHANGE, String.valueOf(timestamp))
242 .build();
243
Yi Tsenge616d752018-11-27 10:53:27 -0800244 // Port/Interface name is identical in OpenConfig model, but not in ONOS
245 // This might cause some problem if we use one name to different port
246 PortDescription portDescription = DefaultPortDescription.builder()
247 .portSpeed(port.portSpeed())
248 .withPortNumber(port.number())
249 .isEnabled(update.getVal().getStringVal().equals("UP"))
250 .type(port.type())
Yi Tseng59d5f3e2018-11-27 23:09:41 -0800251 .annotations(portAnnotations)
Yi Tsenge616d752018-11-27 10:53:27 -0800252 .build();
253 providerService.portStatusChanged(deviceId, portDescription);
254 });
255 }
256
257 class InternalGnmiEventListener implements GnmiEventListener {
258
259 @Override
260 public void event(GnmiEvent event) {
pierventre52ef9332021-07-09 22:42:17 +0200261 eventExecutor.execute(() -> {
262 if (!deviceSubscribed.containsKey(event.subject().deviceId())) {
263 log.warn("Received gNMI event from {}, but we did'nt expect to " +
264 "be subscribed to it! Discarding event...",
265 event.subject().deviceId());
266 return;
267 }
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800268
pierventre52ef9332021-07-09 22:42:17 +0200269 log.debug("Received gNMI event {}", event.toString());
270 if (event.type() == GnmiEvent.Type.UPDATE) {
271 handleGnmiUpdate((GnmiUpdate) event.subject());
272 } else {
273 log.debug("Unsupported gNMI event type: {}", event.type());
274 }
275 });
Yi Tsenge616d752018-11-27 10:53:27 -0800276 }
277 }
278
279 class InternalMastershipListener implements MastershipListener {
280
281 @Override
282 public void event(MastershipEvent event) {
pierventre52ef9332021-07-09 22:42:17 +0200283 eventExecutor.execute(() -> checkSubscription(event.subject()));
Yi Tsenge616d752018-11-27 10:53:27 -0800284 }
285 }
286
287 class InternalDeviceListener implements DeviceListener {
288
289 @Override
290 public void event(DeviceEvent event) {
pierventre52ef9332021-07-09 22:42:17 +0200291 eventExecutor.execute(() -> {
292 switch (event.type()) {
293 case DEVICE_ADDED:
294 case DEVICE_AVAILABILITY_CHANGED:
295 case DEVICE_UPDATED:
296 case DEVICE_REMOVED:
297 case PORT_ADDED:
298 case PORT_REMOVED:
299 checkSubscription(event.subject().id());
300 break;
301 default:
302 break;
303 }
304 });
Yi Tsenge616d752018-11-27 10:53:27 -0800305 }
306 }
307}