blob: c6f3d04751e85ed25e00bbfe9a7b6ef60f149dac [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 Cascone6256d012016-06-17 13:49:52 -0700174 private void configCheck(DeviceId deviceId, Bmv2DeviceContext storedContext) {
175 if (storedContext == null) {
176 return;
177 }
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700178 // Synchronize executions over the same deviceId.
Carmelo Cascone6256d012016-06-17 13:49:52 -0700179 Lock lock = deviceLocks.computeIfAbsent(deviceId, did -> new ReentrantLock());
180 lock.lock();
181 try {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700182 log.trace("Executing configuration check on {}...", deviceId);
183
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700184 try {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700185 // FIXME: JSON dump is heavy, can we use the JSON MD5 to check the running configuration?
186 String jsonString = controller.getAgent(deviceId).dumpJsonConfig();
187 Bmv2Configuration deviceConfiguration = parse(Json.parse(jsonString).asObject());
188
189 if (!storedContext.configuration().equals(deviceConfiguration)) {
190 log.info("Triggering configuration swap on {}...", deviceId);
191 try {
192 Bmv2DeviceAgent agent = controller.getAgent(deviceId);
193 String newJsonString = storedContext.configuration().json().toString();
194 agent.uploadNewJsonConfig(newJsonString);
195 agent.swapJsonConfig();
196 } catch (Bmv2RuntimeException e) {
197 log.error("Unable to swap configuration on {}: {}", deviceId, e.explain());
198 }
199 }
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700200 } catch (Bmv2RuntimeException e) {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700201 log.warn("Unable to dump JSON configuration from {}: {}", deviceId, e.explain());
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700202 }
Carmelo Cascone6256d012016-06-17 13:49:52 -0700203 } finally {
204 lock.unlock();
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700205 }
206 }
207
Carmelo Cascone6256d012016-06-17 13:49:52 -0700208 private void triggerConfigCheck(DeviceId deviceId, Bmv2DeviceContext context) {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700209 if (mastershipService.isLocalMaster(deviceId)) {
Carmelo Cascone6256d012016-06-17 13:49:52 -0700210 scheduledExecutor.schedule(() -> configCheck(deviceId, context), 0, TimeUnit.SECONDS);
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700211 }
212 }
213
214 private void checkDevices() {
215 deviceService.getAvailableDevices().forEach(device -> {
Carmelo Cascone6256d012016-06-17 13:49:52 -0700216 triggerConfigCheck(device.id(), getContext(device.id()));
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700217 });
218 }
219
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700220 protected static Bmv2DefaultConfiguration loadDefaultConfiguration() {
221 try {
222 JsonObject json = Json.parse(new BufferedReader(new InputStreamReader(
223 Bmv2DeviceContextServiceImpl.class.getResourceAsStream(JSON_DEFAULT_CONFIG_PATH)))).asObject();
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700224 return parse(json);
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700225 } catch (IOException e) {
226 throw new RuntimeException("Unable to load default configuration", e);
227 }
228 }
229
230 /**
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700231 * Listener of context changes that immediately triggers config checks (to swap the config if necessary).
232 */
233 private class ContextMapEventListener implements MapEventListener<DeviceId, Bmv2DeviceContext> {
234 @Override
235 public void event(MapEvent<DeviceId, Bmv2DeviceContext> event) {
236 DeviceId deviceId = event.key();
237 if (event.type().equals(INSERT) || event.type().equals(UPDATE)) {
238 log.trace("Context {} for {}", event.type().name(), deviceId);
Carmelo Cascone6256d012016-06-17 13:49:52 -0700239 triggerConfigCheck(deviceId, event.newValue().value());
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700240 }
241 }
242 }
243
244 /**
245 * Context serializer.
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700246 */
247 private class BmvDeviceContextSerializer extends com.esotericsoftware.kryo.Serializer<Bmv2DeviceContext> {
248
249 @Override
250 public void write(Kryo kryo, Output output, Bmv2DeviceContext context) {
251 kryo.writeObject(output, context.configuration().json().toString());
252 kryo.writeObject(output, context.interpreter().getClass().getName());
253 }
254
255 @Override
256 public Bmv2DeviceContext read(Kryo kryo, Input input, Class<Bmv2DeviceContext> type) {
257 String jsonStr = kryo.readObject(input, String.class);
258 String interpreterClassName = kryo.readObject(input, String.class);
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700259 Bmv2Configuration configuration = parse(Json.parse(jsonStr).asObject());
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700260 ClassLoader loader = interpreterClassLoaders.get(interpreterClassName);
261 if (loader == null) {
262 throw new IllegalStateException("No class loader registered for interpreter: " + interpreterClassName);
263 }
264 try {
265 Bmv2Interpreter interpreter = (Bmv2Interpreter) loader.loadClass(interpreterClassName).newInstance();
266 return new Bmv2DeviceContext(configuration, interpreter);
267 } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
268 throw new RuntimeException("Unable to load interpreter class", e);
269 }
270 }
271 }
272}