blob: d55ca7f748828ce0871934050380873c91d44d49 [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;
Yi Tsenge616d752018-11-27 10:53:27 -080055import java.util.concurrent.locks.Lock;
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080056import java.util.stream.Collectors;
Yi Tsenge616d752018-11-27 10:53:27 -080057
58/**
59 * Entity that manages gNMI subscription for devices using OpenConfig models and
60 * that reports relevant events to the core.
61 */
62@Beta
63class GnmiDeviceStateSubscriber {
64
pierventre50696a72021-06-01 12:26:36 +020065 private static final String LAST_CHANGE = "last-change";
Yi Tseng59d5f3e2018-11-27 23:09:41 -080066
Yi Tsenge616d752018-11-27 10:53:27 -080067 private static Logger log = LoggerFactory.getLogger(GnmiDeviceStateSubscriber.class);
68
69 private final GnmiController gnmiController;
70 private final DeviceService deviceService;
71 private final DeviceProviderService providerService;
72 private final MastershipService mastershipService;
73
Yi Tsenge616d752018-11-27 10:53:27 -080074 private final InternalGnmiEventListener gnmiEventListener = new InternalGnmiEventListener();
75 private final InternalDeviceListener deviceEventListener = new InternalDeviceListener();
76 private final InternalMastershipListener mastershipListener = new InternalMastershipListener();
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080077 private final Map<DeviceId, Set<PortNumber>> deviceSubscribed = Maps.newHashMap();
Yi Tsenge616d752018-11-27 10:53:27 -080078
79 private final Striped<Lock> deviceLocks = Striped.lock(30);
80
81 GnmiDeviceStateSubscriber(GnmiController gnmiController, DeviceService deviceService,
82 MastershipService mastershipService,
83 DeviceProviderService providerService) {
84 this.gnmiController = gnmiController;
85 this.deviceService = deviceService;
86 this.mastershipService = mastershipService;
87 this.providerService = providerService;
88 }
89
90 public void activate() {
91 deviceService.addListener(deviceEventListener);
92 mastershipService.addListener(mastershipListener);
93 gnmiController.addListener(gnmiEventListener);
94 // Subscribe to existing devices.
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080095 deviceService.getDevices().forEach(d -> checkSubscription(d.id()));
Yi Tsenge616d752018-11-27 10:53:27 -080096 }
97
98 public void deactivate() {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -080099 deviceSubscribed.keySet().forEach(this::unsubscribeIfNeeded);
Yi Tsenge616d752018-11-27 10:53:27 -0800100 deviceService.removeListener(deviceEventListener);
101 mastershipService.removeListener(mastershipListener);
102 gnmiController.removeListener(gnmiEventListener);
103 }
104
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800105 private void checkSubscription(DeviceId deviceId) {
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700106 if (gnmiController.get(deviceId) == null) {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800107 // Ignore devices for which a gNMI client does not exist.
108 return;
109 }
Yi Tsenge616d752018-11-27 10:53:27 -0800110 deviceLocks.get(deviceId).lock();
111 try {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800112 if (shouldHaveSubscription(deviceId)) {
Yi Tsenge616d752018-11-27 10:53:27 -0800113 subscribeIfNeeded(deviceId);
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800114 } else {
115 unsubscribeIfNeeded(deviceId);
Yi Tsenge616d752018-11-27 10:53:27 -0800116 }
117 } finally {
118 deviceLocks.get(deviceId).unlock();
119 }
120 }
121
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800122 private boolean shouldHaveSubscription(DeviceId deviceId) {
123 return deviceService.getDevice(deviceId) != null
124 && deviceService.isAvailable(deviceId)
125 && mastershipService.isLocalMaster(deviceId)
126 && !deviceService.getPorts(deviceId).isEmpty();
127 }
128
pierventre50696a72021-06-01 12:26:36 +0200129 private Path interfaceStatePath(String interfaceName) {
Yi Tsenge616d752018-11-27 10:53:27 -0800130 return Path.newBuilder()
131 .addElem(PathElem.newBuilder().setName("interfaces").build())
132 .addElem(PathElem.newBuilder()
133 .setName("interface").putKey("name", interfaceName).build())
134 .addElem(PathElem.newBuilder().setName("state").build())
Yi Tsenge616d752018-11-27 10:53:27 -0800135 .build();
136 }
137
138 private void unsubscribeIfNeeded(DeviceId deviceId) {
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700139 gnmiController.get(deviceId).unsubscribe();
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800140 if (deviceSubscribed.remove(deviceId) != null) {
141 log.info("Cancelled gNMI subscription for {}", deviceId);
Yi Tsenge616d752018-11-27 10:53:27 -0800142 }
Yi Tsenge616d752018-11-27 10:53:27 -0800143 }
144
145 private void subscribeIfNeeded(DeviceId deviceId) {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800146
147 Set<PortNumber> ports = deviceService.getPorts(deviceId).stream()
148 .map(Port::number)
149 .collect(Collectors.toSet());
150
151 if (Objects.equals(ports, deviceSubscribed.get(deviceId))) {
152 // Already subscribed for the same ports.
Yi Tsenge616d752018-11-27 10:53:27 -0800153 return;
154 }
155
Carmelo Cascone67a317d2019-03-18 17:08:22 -0700156 // Subscribe for the new set of ports.
157 deviceSubscribed.put(deviceId, ports);
Yi Tsenge616d752018-11-27 10:53:27 -0800158
Carmelo Cascone67a317d2019-03-18 17:08:22 -0700159 // Send subscription request.
160 final SubscriptionList subscriptionList = SubscriptionList.newBuilder()
161 .setMode(SubscriptionList.Mode.STREAM)
162 .setUpdatesOnly(true)
163 .addAllSubscription(ports.stream().map(
164 port -> Subscription.newBuilder()
pierventre50696a72021-06-01 12:26:36 +0200165 .setPath(interfaceStatePath(port.name()))
Carmelo Cascone67a317d2019-03-18 17:08:22 -0700166 .setMode(SubscriptionMode.ON_CHANGE)
167 .build()).collect(Collectors.toList()))
Yi Tsenge616d752018-11-27 10:53:27 -0800168 .build();
Carmelo Casconec2be50a2019-04-10 00:15:39 -0700169 gnmiController.get(deviceId).subscribe(
Carmelo Cascone67a317d2019-03-18 17:08:22 -0700170 SubscribeRequest.newBuilder()
171 .setSubscribe(subscriptionList)
172 .build());
Yi Tsenge616d752018-11-27 10:53:27 -0800173
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800174 log.info("Started gNMI subscription for {} ports on {}", ports.size(), deviceId);
Yi Tsenge616d752018-11-27 10:53:27 -0800175 }
176
177 private void handleGnmiUpdate(GnmiUpdate eventSubject) {
178 Notification notification = eventSubject.update();
179 if (notification == null) {
180 log.warn("Cannot handle gNMI event without update data, abort");
181 log.debug("gNMI update:\n{}", eventSubject);
182 return;
183 }
184
pierventre50696a72021-06-01 12:26:36 +0200185 long lastChange = 0;
186 Update statusUpdate = null;
187 Path path;
188 PathElem lastElem;
189 // The assumption is that the notification contains all the updates:
190 // last-change, oper-status, counters, and so on. Otherwise, we need
191 // to put in place the aggregation logic in ONOS
192 for (Update update : notification.getUpdateList()) {
193 path = update.getPath();
194 lastElem = path.getElem(path.getElemCount() - 1);
Yi Tsenge616d752018-11-27 10:53:27 -0800195
196 // Use last element to identify which state updated
197 if ("oper-status".equals(lastElem.getName())) {
pierventre50696a72021-06-01 12:26:36 +0200198 statusUpdate = update;
199 } else if ("last-change".equals(lastElem.getName())) {
200 lastChange = update.getVal().getUintVal();
201 } else if (log.isDebugEnabled()) {
Yi Tsenge616d752018-11-27 10:53:27 -0800202 log.debug("Unrecognized update {}", GnmiUtils.pathToString(path));
203 }
pierventre50696a72021-06-01 12:26:36 +0200204 }
205
206 // Last-change could be not supported by the device
207 // Cannot proceed without the status update.
208 if (statusUpdate != null) {
209 handleOperStatusUpdate(eventSubject.deviceId(), statusUpdate, lastChange);
210 }
Yi Tsenge616d752018-11-27 10:53:27 -0800211 }
212
Yi Tseng59d5f3e2018-11-27 23:09:41 -0800213 private void handleOperStatusUpdate(DeviceId deviceId, Update update, long timestamp) {
Yi Tsenge616d752018-11-27 10:53:27 -0800214 Path path = update.getPath();
215 // first element should be "interface"
216 String interfaceName = path.getElem(1).getKeyOrDefault("name", null);
217 if (interfaceName == null) {
218 log.error("No interface present in gNMI update, abort");
219 log.debug("gNMI update:\n{}", update);
220 return;
221 }
222
223 List<Port> portsFromDevice = deviceService.getPorts(deviceId);
224 portsFromDevice.forEach(port -> {
225 if (!port.number().name().equals(interfaceName)) {
226 return;
227 }
228
Yi Tseng59d5f3e2018-11-27 23:09:41 -0800229 DefaultAnnotations portAnnotations = DefaultAnnotations.builder()
230 .putAll(port.annotations())
231 .set(LAST_CHANGE, String.valueOf(timestamp))
232 .build();
233
Yi Tsenge616d752018-11-27 10:53:27 -0800234 // Port/Interface name is identical in OpenConfig model, but not in ONOS
235 // This might cause some problem if we use one name to different port
236 PortDescription portDescription = DefaultPortDescription.builder()
237 .portSpeed(port.portSpeed())
238 .withPortNumber(port.number())
239 .isEnabled(update.getVal().getStringVal().equals("UP"))
240 .type(port.type())
Yi Tseng59d5f3e2018-11-27 23:09:41 -0800241 .annotations(portAnnotations)
Yi Tsenge616d752018-11-27 10:53:27 -0800242 .build();
243 providerService.portStatusChanged(deviceId, portDescription);
244 });
245 }
246
247 class InternalGnmiEventListener implements GnmiEventListener {
248
249 @Override
250 public void event(GnmiEvent event) {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800251 if (!deviceSubscribed.containsKey(event.subject().deviceId())) {
252 log.warn("Received gNMI event from {}, but we did'nt expect to " +
253 "be subscribed to it! Discarding event...",
Yi Tsenge616d752018-11-27 10:53:27 -0800254 event.subject().deviceId());
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800255 return;
Yi Tsenge616d752018-11-27 10:53:27 -0800256 }
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800257
Yi Tsenge616d752018-11-27 10:53:27 -0800258 log.debug("Received gNMI event {}", event.toString());
259 if (event.type() == GnmiEvent.Type.UPDATE) {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800260 handleGnmiUpdate((GnmiUpdate) event.subject());
Yi Tsenge616d752018-11-27 10:53:27 -0800261 } else {
262 log.debug("Unsupported gNMI event type: {}", event.type());
263 }
264 }
265 }
266
267 class InternalMastershipListener implements MastershipListener {
268
269 @Override
270 public void event(MastershipEvent event) {
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800271 checkSubscription(event.subject());
Yi Tsenge616d752018-11-27 10:53:27 -0800272 }
273 }
274
275 class InternalDeviceListener implements DeviceListener {
276
277 @Override
278 public void event(DeviceEvent event) {
279 switch (event.type()) {
280 case DEVICE_ADDED:
281 case DEVICE_AVAILABILITY_CHANGED:
282 case DEVICE_UPDATED:
283 case DEVICE_REMOVED:
Carmelo Casconeab5d41e2019-03-06 18:02:34 -0800284 case PORT_ADDED:
285 case PORT_REMOVED:
286 checkSubscription(event.subject().id());
Yi Tsenge616d752018-11-27 10:53:27 -0800287 break;
288 default:
289 break;
290 }
291 }
292 }
293}