blob: 4727913e8748922978e833f00e939bf5a7c8386b [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 Cascone17fc9e42016-05-31 11:29:21 -070063
64import static com.google.common.base.Preconditions.checkNotNull;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070065import static org.onosproject.bmv2.api.context.Bmv2DefaultConfiguration.parse;
66import static org.onosproject.store.service.MapEvent.Type.INSERT;
67import static org.onosproject.store.service.MapEvent.Type.UPDATE;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070068
69@Component(immediate = true)
70@Service
71public class Bmv2DeviceContextServiceImpl implements Bmv2DeviceContextService {
72
73 private static final String JSON_DEFAULT_CONFIG_PATH = "/default.json";
Carmelo Casconec18e82c2016-06-16 14:22:36 -070074 private static final long CHECK_INTERVAL = 5_000; // milliseconds
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070075
76 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
77 private StorageService storageService;
78
79 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
Carmelo Casconec18e82c2016-06-16 14:22:36 -070080 private DeviceService deviceService;
81
82 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
83 private MastershipService mastershipService;
84
85 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070086 private Bmv2Controller controller;
87
Carmelo Casconec18e82c2016-06-16 14:22:36 -070088 private final ScheduledExecutorService scheduledExecutor = SharedScheduledExecutors.getPoolThreadExecutor();
89 private final MapEventListener<DeviceId, Bmv2DeviceContext> contextListener = new ContextMapEventListener();
90 private final ConcurrentMap<DeviceId, Boolean> deviceLocks = Maps.newConcurrentMap();
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070091
92 private ConsistentMap<DeviceId, Bmv2DeviceContext> contexts;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070093 private Map<String, ClassLoader> interpreterClassLoaders;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070094 private Bmv2DeviceContext defaultContext;
Carmelo Casconec18e82c2016-06-16 14:22:36 -070095 private ScheduledFuture<?> configChecker = null;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -070096
97 private final Logger log = LoggerFactory.getLogger(getClass());
98
99 @Activate
100 public void activate() {
101 KryoNamespace kryo = new KryoNamespace.Builder()
102 .register(KryoNamespaces.API)
103 .register(new BmvDeviceContextSerializer(), Bmv2DeviceContext.class)
104 .build();
105
106 this.contexts = storageService.<DeviceId, Bmv2DeviceContext>consistentMapBuilder()
107 .withSerializer(Serializer.using(kryo))
108 .withName("onos-bmv2-contexts")
109 .build();
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700110
111 Bmv2Configuration defaultConfiguration = loadDefaultConfiguration();
112 Bmv2Interpreter defaultInterpreter = new Bmv2DefaultInterpreterImpl();
113 defaultContext = new Bmv2DeviceContext(defaultConfiguration, defaultInterpreter);
114
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700115 interpreterClassLoaders = Maps.newConcurrentMap();
116 registerInterpreterClassLoader(defaultInterpreter.getClass(), this.getClass().getClassLoader());
117
118 contexts.addListener(contextListener);
119
120 if (configChecker != null && configChecker.isCancelled()) {
121 configChecker.cancel(false);
122 }
123 configChecker = scheduledExecutor.scheduleAtFixedRate(this::checkDevices, 0, CHECK_INTERVAL,
124 TimeUnit.MILLISECONDS);
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700125
126 log.info("Started");
127 }
128
129 @Deactivate
130 public void deactivate() {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700131 contexts.removeListener(contextListener);
132 if (configChecker != null) {
133 configChecker.cancel(false);
134 }
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700135 log.info("Stopped");
136 }
137
138 @Override
139 public Bmv2DeviceContext getContext(DeviceId deviceId) {
140 checkNotNull(deviceId, "device id cannot be null");
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700141 Versioned<Bmv2DeviceContext> versionedContext = contexts.get(deviceId);
142 return (versionedContext == null) ? null : versionedContext.value();
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700143 }
144
145 @Override
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700146 public void setContext(DeviceId deviceId, Bmv2DeviceContext context) {
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700147 checkNotNull(deviceId, "device id cannot be null");
148 checkNotNull(context, "context cannot be null");
149 if (!interpreterClassLoaders.containsKey(context.interpreter().getClass().getName())) {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700150 log.error("Unable to set context, missing class loader for interpreter '{}'. " +
151 "Please register it with registerInterpreterClassLoader()",
152 context.interpreter().getClass().getName());
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700153 } else {
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700154 try {
155 contexts.put(deviceId, context);
156 } catch (ConsistentMapException.ConcurrentModification e) {
157 log.error("Detected concurrent modification on context map");
158 }
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700159 }
160 }
161
162 @Override
163 public void registerInterpreterClassLoader(Class<? extends Bmv2Interpreter> interpreterClass, ClassLoader loader) {
164 interpreterClassLoaders.put(interpreterClass.getName(), loader);
165 }
166
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700167 @Override
168 public Bmv2DeviceContext defaultContext() {
169 return defaultContext;
170 }
171
172 private void configCheck(DeviceId deviceId) {
173 // Synchronize executions over the same deviceId.
174 deviceLocks.putIfAbsent(deviceId, new Boolean(true));
175 synchronized (deviceLocks.get(deviceId)) {
176
177 Bmv2DeviceContext storedContext = getContext(deviceId);
178 if (storedContext == null) {
179 return;
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700180 }
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700181
182 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 Casconec18e82c2016-06-16 14:22:36 -0700203 }
204 }
205
206 private void triggerConfigCheck(DeviceId deviceId) {
207 if (mastershipService.isLocalMaster(deviceId)) {
208 scheduledExecutor.schedule(() -> configCheck(deviceId), 0, TimeUnit.SECONDS);
209 }
210 }
211
212 private void checkDevices() {
213 deviceService.getAvailableDevices().forEach(device -> {
214 triggerConfigCheck(device.id());
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700215 });
216 }
217
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700218 protected static Bmv2DefaultConfiguration loadDefaultConfiguration() {
219 try {
220 JsonObject json = Json.parse(new BufferedReader(new InputStreamReader(
221 Bmv2DeviceContextServiceImpl.class.getResourceAsStream(JSON_DEFAULT_CONFIG_PATH)))).asObject();
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700222 return parse(json);
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700223 } catch (IOException e) {
224 throw new RuntimeException("Unable to load default configuration", e);
225 }
226 }
227
228 /**
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700229 * Listener of context changes that immediately triggers config checks (to swap the config if necessary).
230 */
231 private class ContextMapEventListener implements MapEventListener<DeviceId, Bmv2DeviceContext> {
232 @Override
233 public void event(MapEvent<DeviceId, Bmv2DeviceContext> event) {
234 DeviceId deviceId = event.key();
235 if (event.type().equals(INSERT) || event.type().equals(UPDATE)) {
236 log.trace("Context {} for {}", event.type().name(), deviceId);
237 triggerConfigCheck(deviceId);
238 }
239 }
240 }
241
242 /**
243 * Context serializer.
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700244 */
245 private class BmvDeviceContextSerializer extends com.esotericsoftware.kryo.Serializer<Bmv2DeviceContext> {
246
247 @Override
248 public void write(Kryo kryo, Output output, Bmv2DeviceContext context) {
249 kryo.writeObject(output, context.configuration().json().toString());
250 kryo.writeObject(output, context.interpreter().getClass().getName());
251 }
252
253 @Override
254 public Bmv2DeviceContext read(Kryo kryo, Input input, Class<Bmv2DeviceContext> type) {
255 String jsonStr = kryo.readObject(input, String.class);
256 String interpreterClassName = kryo.readObject(input, String.class);
Carmelo Casconec18e82c2016-06-16 14:22:36 -0700257 Bmv2Configuration configuration = parse(Json.parse(jsonStr).asObject());
Carmelo Cascone17fc9e42016-05-31 11:29:21 -0700258 ClassLoader loader = interpreterClassLoaders.get(interpreterClassName);
259 if (loader == null) {
260 throw new IllegalStateException("No class loader registered for interpreter: " + interpreterClassName);
261 }
262 try {
263 Bmv2Interpreter interpreter = (Bmv2Interpreter) loader.loadClass(interpreterClassName).newInstance();
264 return new Bmv2DeviceContext(configuration, interpreter);
265 } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
266 throw new RuntimeException("Unable to load interpreter class", e);
267 }
268 }
269 }
270}