blob: e077b6d062d36dd8a6309c1a847e93b801e0bbd3 [file] [log] [blame]
Carmelo Cascone17fc9e42016-05-31 11:29:21 -07001/*
2 * Copyright 2016-present Open Networking Laboratory
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.bmv2.ctl;
18
19import com.eclipsesource.json.Json;
20import com.eclipsesource.json.JsonObject;
21import com.esotericsoftware.kryo.Kryo;
22import com.esotericsoftware.kryo.io.Input;
23import com.esotericsoftware.kryo.io.Output;
24import com.google.common.collect.Maps;
25import org.apache.felix.scr.annotations.Activate;
26import org.apache.felix.scr.annotations.Component;
27import org.apache.felix.scr.annotations.Deactivate;
28import org.apache.felix.scr.annotations.Reference;
29import org.apache.felix.scr.annotations.ReferenceCardinality;
30import org.apache.felix.scr.annotations.Service;
31import org.onlab.util.KryoNamespace;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070032import org.onlab.util.SharedScheduledExecutors;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070033import org.onosproject.bmv2.api.context.Bmv2Configuration;
34import org.onosproject.bmv2.api.context.Bmv2DefaultConfiguration;
35import org.onosproject.bmv2.api.context.Bmv2DeviceContext;
36import org.onosproject.bmv2.api.context.Bmv2Interpreter;
37import org.onosproject.bmv2.api.runtime.Bmv2DeviceAgent;
38import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
39import org.onosproject.bmv2.api.service.Bmv2Controller;
40import org.onosproject.bmv2.api.service.Bmv2DeviceContextService;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070041import org.onosproject.mastership.MastershipService;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070042import org.onosproject.net.DeviceId;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070043import org.onosproject.net.device.DeviceService;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070044import org.onosproject.store.serializers.KryoNamespaces;
45import org.onosproject.store.service.ConsistentMap;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070046import org.onosproject.store.service.ConsistentMapException;
47import org.onosproject.store.service.MapEvent;
48import org.onosproject.store.service.MapEventListener;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070049import org.onosproject.store.service.Serializer;
50import org.onosproject.store.service.StorageService;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070051import org.onosproject.store.service.Versioned;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070052import org.slf4j.Logger;
53import org.slf4j.LoggerFactory;
54
55import java.io.BufferedReader;
56import java.io.IOException;
57import java.io.InputStreamReader;
58import java.util.Map;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070059import java.util.concurrent.ConcurrentMap;
60import java.util.concurrent.ScheduledExecutorService;
61import java.util.concurrent.ScheduledFuture;
62import java.util.concurrent.TimeUnit;
Carmelo Cascone6256d012016-06-17 13:49:52 -070063import java.util.concurrent.locks.Lock;
64import java.util.concurrent.locks.ReentrantLock;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070065
66import static com.google.common.base.Preconditions.checkNotNull;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070067import static org.onosproject.bmv2.api.context.Bmv2DefaultConfiguration.parse;
68import static org.onosproject.store.service.MapEvent.Type.INSERT;
69import static org.onosproject.store.service.MapEvent.Type.UPDATE;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070070
71@Component(immediate = true)
72@Service
73public class Bmv2DeviceContextServiceImpl implements Bmv2DeviceContextService {
74
75 private static final String JSON_DEFAULT_CONFIG_PATH = "/default.json";
Carmelo Casconec18e82c2016-06-16 14:22:36 -070076 private static final long CHECK_INTERVAL = 5_000; // milliseconds
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070077
78 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
79 private StorageService storageService;
80
81 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
Carmelo Casconec18e82c2016-06-16 14:22:36 -070082 private DeviceService deviceService;
83
84 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
85 private MastershipService mastershipService;
86
87 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070088 private Bmv2Controller controller;
89
Carmelo Casconec18e82c2016-06-16 14:22:36 -070090 private final ScheduledExecutorService scheduledExecutor = SharedScheduledExecutors.getPoolThreadExecutor();
91 private final MapEventListener<DeviceId, Bmv2DeviceContext> contextListener = new ContextMapEventListener();
Carmelo Cascone6256d012016-06-17 13:49:52 -070092 private final ConcurrentMap<DeviceId, Lock> deviceLocks = Maps.newConcurrentMap();
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070093
94 private ConsistentMap<DeviceId, Bmv2DeviceContext> contexts;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070095 private Map<String, ClassLoader> interpreterClassLoaders;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070096 private Bmv2DeviceContext defaultContext;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070097 private ScheduledFuture<?> configChecker = null;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070098
99 private final Logger log = LoggerFactory.getLogger(getClass());
100
101 @Activate
102 public void activate() {
103 KryoNamespace kryo = new KryoNamespace.Builder()
104 .register(KryoNamespaces.API)
105 .register(new BmvDeviceContextSerializer(), Bmv2DeviceContext.class)
106 .build();
107
108 this.contexts = storageService.<DeviceId, Bmv2DeviceContext>consistentMapBuilder()
109 .withSerializer(Serializer.using(kryo))
110 .withName("onos-bmv2-contexts")
111 .build();
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700112
113 Bmv2Configuration defaultConfiguration = loadDefaultConfiguration();
114 Bmv2Interpreter defaultInterpreter = new Bmv2DefaultInterpreterImpl();
115 defaultContext = new Bmv2DeviceContext(defaultConfiguration, defaultInterpreter);
116
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700117 interpreterClassLoaders = Maps.newConcurrentMap();
118 registerInterpreterClassLoader(defaultInterpreter.getClass(), this.getClass().getClassLoader());
119
Carmelo Cascone6256d012016-06-17 13:49:52 -0700120 contexts.addListener(contextListener, scheduledExecutor);
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700121
122 if (configChecker != null && configChecker.isCancelled()) {
123 configChecker.cancel(false);
124 }
125 configChecker = scheduledExecutor.scheduleAtFixedRate(this::checkDevices, 0, CHECK_INTERVAL,
126 TimeUnit.MILLISECONDS);
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700127
128 log.info("Started");
129 }
130
131 @Deactivate
132 public void deactivate() {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700133 contexts.removeListener(contextListener);
134 if (configChecker != null) {
135 configChecker.cancel(false);
136 }
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700137 log.info("Stopped");
138 }
139
140 @Override
141 public Bmv2DeviceContext getContext(DeviceId deviceId) {
142 checkNotNull(deviceId, "device id cannot be null");
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700143 Versioned<Bmv2DeviceContext> versionedContext = contexts.get(deviceId);
144 return (versionedContext == null) ? null : versionedContext.value();
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700145 }
146
147 @Override
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700148 public void setContext(DeviceId deviceId, Bmv2DeviceContext context) {
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700149 checkNotNull(deviceId, "device id cannot be null");
150 checkNotNull(context, "context cannot be null");
151 if (!interpreterClassLoaders.containsKey(context.interpreter().getClass().getName())) {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700152 log.error("Unable to set context, missing class loader for interpreter '{}'. " +
153 "Please register it with registerInterpreterClassLoader()",
154 context.interpreter().getClass().getName());
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700155 } else {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700156 try {
157 contexts.put(deviceId, context);
158 } catch (ConsistentMapException.ConcurrentModification e) {
159 log.error("Detected concurrent modification on context map");
160 }
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700161 }
162 }
163
164 @Override
165 public void registerInterpreterClassLoader(Class<? extends Bmv2Interpreter> interpreterClass, ClassLoader loader) {
166 interpreterClassLoaders.put(interpreterClass.getName(), loader);
167 }
168
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700169 @Override
170 public Bmv2DeviceContext defaultContext() {
171 return defaultContext;
172 }
173
Carmelo Cascone62f1e1e2016-06-22 01:43:49 -0700174 @Override
175 public void setDefaultContext(DeviceId deviceId) {
176 Versioned<Bmv2DeviceContext> previous = contexts.put(deviceId, defaultContext);
177 if (mastershipService.getMasterFor(deviceId) == null) {
178 // Checking for who is the master here is ugly but necessary, as this method is called by Bmv2DeviceProvider
179 // prior to master election. A solution could be to use a separate leadership contest instead of the
180 // mastership service.
181 triggerConfigCheck(deviceId, defaultContext);
182 }
183 }
184
Carmelo Cascone6256d012016-06-17 13:49:52 -0700185 private void configCheck(DeviceId deviceId, Bmv2DeviceContext storedContext) {
186 if (storedContext == null) {
187 return;
188 }
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700189 // Synchronize executions over the same deviceId.
Carmelo Cascone6256d012016-06-17 13:49:52 -0700190 Lock lock = deviceLocks.computeIfAbsent(deviceId, did -> new ReentrantLock());
191 lock.lock();
192 try {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700193 log.trace("Executing configuration check on {}...", deviceId);
194
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700195 try {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700196 // FIXME: JSON dump is heavy, can we use the JSON MD5 to check the running configuration?
197 String jsonString = controller.getAgent(deviceId).dumpJsonConfig();
198 Bmv2Configuration deviceConfiguration = parse(Json.parse(jsonString).asObject());
199
200 if (!storedContext.configuration().equals(deviceConfiguration)) {
201 log.info("Triggering configuration swap on {}...", deviceId);
202 try {
203 Bmv2DeviceAgent agent = controller.getAgent(deviceId);
204 String newJsonString = storedContext.configuration().json().toString();
205 agent.uploadNewJsonConfig(newJsonString);
206 agent.swapJsonConfig();
207 } catch (Bmv2RuntimeException e) {
208 log.error("Unable to swap configuration on {}: {}", deviceId, e.explain());
209 }
210 }
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700211 } catch (Bmv2RuntimeException e) {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700212 log.warn("Unable to dump JSON configuration from {}: {}", deviceId, e.explain());
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700213 }
Carmelo Cascone6256d012016-06-17 13:49:52 -0700214 } finally {
215 lock.unlock();
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700216 }
217 }
218
Carmelo Cascone6256d012016-06-17 13:49:52 -0700219 private void triggerConfigCheck(DeviceId deviceId, Bmv2DeviceContext context) {
Carmelo Cascone6256d012016-06-17 13:49:52 -0700220 scheduledExecutor.schedule(() -> configCheck(deviceId, context), 0, TimeUnit.SECONDS);
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700221 }
222
223 private void checkDevices() {
224 deviceService.getAvailableDevices().forEach(device -> {
Carmelo Cascone62f1e1e2016-06-22 01:43:49 -0700225 if (mastershipService.isLocalMaster(device.id())) {
226 triggerConfigCheck(device.id(), getContext(device.id()));
227 }
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700228 });
229 }
230
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700231 protected static Bmv2DefaultConfiguration loadDefaultConfiguration() {
232 try {
233 JsonObject json = Json.parse(new BufferedReader(new InputStreamReader(
234 Bmv2DeviceContextServiceImpl.class.getResourceAsStream(JSON_DEFAULT_CONFIG_PATH)))).asObject();
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700235 return parse(json);
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700236 } catch (IOException e) {
237 throw new RuntimeException("Unable to load default configuration", e);
238 }
239 }
240
241 /**
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700242 * Listener of context changes that immediately triggers config checks (to swap the config if necessary).
243 */
244 private class ContextMapEventListener implements MapEventListener<DeviceId, Bmv2DeviceContext> {
245 @Override
246 public void event(MapEvent<DeviceId, Bmv2DeviceContext> event) {
247 DeviceId deviceId = event.key();
248 if (event.type().equals(INSERT) || event.type().equals(UPDATE)) {
Carmelo Cascone62f1e1e2016-06-22 01:43:49 -0700249 if (mastershipService.isLocalMaster(deviceId)) {
250 log.trace("Context {} for {}", event.type().name(), deviceId);
251 triggerConfigCheck(deviceId, event.newValue().value());
252 }
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700253 }
254 }
255 }
256
257 /**
258 * Context serializer.
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700259 */
260 private class BmvDeviceContextSerializer extends com.esotericsoftware.kryo.Serializer<Bmv2DeviceContext> {
261
262 @Override
263 public void write(Kryo kryo, Output output, Bmv2DeviceContext context) {
264 kryo.writeObject(output, context.configuration().json().toString());
265 kryo.writeObject(output, context.interpreter().getClass().getName());
266 }
267
268 @Override
269 public Bmv2DeviceContext read(Kryo kryo, Input input, Class<Bmv2DeviceContext> type) {
270 String jsonStr = kryo.readObject(input, String.class);
271 String interpreterClassName = kryo.readObject(input, String.class);
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700272 Bmv2Configuration configuration = parse(Json.parse(jsonStr).asObject());
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700273 ClassLoader loader = interpreterClassLoaders.get(interpreterClassName);
274 if (loader == null) {
275 throw new IllegalStateException("No class loader registered for interpreter: " + interpreterClassName);
276 }
277 try {
278 Bmv2Interpreter interpreter = (Bmv2Interpreter) loader.loadClass(interpreterClassName).newInstance();
279 return new Bmv2DeviceContext(configuration, interpreter);
280 } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
281 throw new RuntimeException("Unable to load interpreter class", e);
282 }
283 }
284 }
285}