blob: 4727913e8748922978e833f00e939bf5a7c8386b [file] [log] [blame]
/*
* Copyright 2016-present Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.bmv2.ctl;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.google.common.collect.Maps;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.Service;
import org.onlab.util.KryoNamespace;
import org.onlab.util.SharedScheduledExecutors;
import org.onosproject.bmv2.api.context.Bmv2Configuration;
import org.onosproject.bmv2.api.context.Bmv2DefaultConfiguration;
import org.onosproject.bmv2.api.context.Bmv2DeviceContext;
import org.onosproject.bmv2.api.context.Bmv2Interpreter;
import org.onosproject.bmv2.api.runtime.Bmv2DeviceAgent;
import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
import org.onosproject.bmv2.api.service.Bmv2Controller;
import org.onosproject.bmv2.api.service.Bmv2DeviceContextService;
import org.onosproject.mastership.MastershipService;
import org.onosproject.net.DeviceId;
import org.onosproject.net.device.DeviceService;
import org.onosproject.store.serializers.KryoNamespaces;
import org.onosproject.store.service.ConsistentMap;
import org.onosproject.store.service.ConsistentMapException;
import org.onosproject.store.service.MapEvent;
import org.onosproject.store.service.MapEventListener;
import org.onosproject.store.service.Serializer;
import org.onosproject.store.service.StorageService;
import org.onosproject.store.service.Versioned;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.onosproject.bmv2.api.context.Bmv2DefaultConfiguration.parse;
import static org.onosproject.store.service.MapEvent.Type.INSERT;
import static org.onosproject.store.service.MapEvent.Type.UPDATE;
@Component(immediate = true)
@Service
public class Bmv2DeviceContextServiceImpl implements Bmv2DeviceContextService {
private static final String JSON_DEFAULT_CONFIG_PATH = "/default.json";
private static final long CHECK_INTERVAL = 5_000; // milliseconds
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
private StorageService storageService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
private DeviceService deviceService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
private MastershipService mastershipService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
private Bmv2Controller controller;
private final ScheduledExecutorService scheduledExecutor = SharedScheduledExecutors.getPoolThreadExecutor();
private final MapEventListener<DeviceId, Bmv2DeviceContext> contextListener = new ContextMapEventListener();
private final ConcurrentMap<DeviceId, Boolean> deviceLocks = Maps.newConcurrentMap();
private ConsistentMap<DeviceId, Bmv2DeviceContext> contexts;
private Map<String, ClassLoader> interpreterClassLoaders;
private Bmv2DeviceContext defaultContext;
private ScheduledFuture<?> configChecker = null;
private final Logger log = LoggerFactory.getLogger(getClass());
@Activate
public void activate() {
KryoNamespace kryo = new KryoNamespace.Builder()
.register(KryoNamespaces.API)
.register(new BmvDeviceContextSerializer(), Bmv2DeviceContext.class)
.build();
this.contexts = storageService.<DeviceId, Bmv2DeviceContext>consistentMapBuilder()
.withSerializer(Serializer.using(kryo))
.withName("onos-bmv2-contexts")
.build();
Bmv2Configuration defaultConfiguration = loadDefaultConfiguration();
Bmv2Interpreter defaultInterpreter = new Bmv2DefaultInterpreterImpl();
defaultContext = new Bmv2DeviceContext(defaultConfiguration, defaultInterpreter);
interpreterClassLoaders = Maps.newConcurrentMap();
registerInterpreterClassLoader(defaultInterpreter.getClass(), this.getClass().getClassLoader());
contexts.addListener(contextListener);
if (configChecker != null && configChecker.isCancelled()) {
configChecker.cancel(false);
}
configChecker = scheduledExecutor.scheduleAtFixedRate(this::checkDevices, 0, CHECK_INTERVAL,
TimeUnit.MILLISECONDS);
log.info("Started");
}
@Deactivate
public void deactivate() {
contexts.removeListener(contextListener);
if (configChecker != null) {
configChecker.cancel(false);
}
log.info("Stopped");
}
@Override
public Bmv2DeviceContext getContext(DeviceId deviceId) {
checkNotNull(deviceId, "device id cannot be null");
Versioned<Bmv2DeviceContext> versionedContext = contexts.get(deviceId);
return (versionedContext == null) ? null : versionedContext.value();
}
@Override
public void setContext(DeviceId deviceId, Bmv2DeviceContext context) {
checkNotNull(deviceId, "device id cannot be null");
checkNotNull(context, "context cannot be null");
if (!interpreterClassLoaders.containsKey(context.interpreter().getClass().getName())) {
log.error("Unable to set context, missing class loader for interpreter '{}'. " +
"Please register it with registerInterpreterClassLoader()",
context.interpreter().getClass().getName());
} else {
try {
contexts.put(deviceId, context);
} catch (ConsistentMapException.ConcurrentModification e) {
log.error("Detected concurrent modification on context map");
}
}
}
@Override
public void registerInterpreterClassLoader(Class<? extends Bmv2Interpreter> interpreterClass, ClassLoader loader) {
interpreterClassLoaders.put(interpreterClass.getName(), loader);
}
@Override
public Bmv2DeviceContext defaultContext() {
return defaultContext;
}
private void configCheck(DeviceId deviceId) {
// Synchronize executions over the same deviceId.
deviceLocks.putIfAbsent(deviceId, new Boolean(true));
synchronized (deviceLocks.get(deviceId)) {
Bmv2DeviceContext storedContext = getContext(deviceId);
if (storedContext == null) {
return;
}
log.trace("Executing configuration check on {}...", deviceId);
try {
// FIXME: JSON dump is heavy, can we use the JSON MD5 to check the running configuration?
String jsonString = controller.getAgent(deviceId).dumpJsonConfig();
Bmv2Configuration deviceConfiguration = parse(Json.parse(jsonString).asObject());
if (!storedContext.configuration().equals(deviceConfiguration)) {
log.info("Triggering configuration swap on {}...", deviceId);
try {
Bmv2DeviceAgent agent = controller.getAgent(deviceId);
String newJsonString = storedContext.configuration().json().toString();
agent.uploadNewJsonConfig(newJsonString);
agent.swapJsonConfig();
} catch (Bmv2RuntimeException e) {
log.error("Unable to swap configuration on {}: {}", deviceId, e.explain());
}
}
} catch (Bmv2RuntimeException e) {
log.warn("Unable to dump JSON configuration from {}: {}", deviceId, e.explain());
}
}
}
private void triggerConfigCheck(DeviceId deviceId) {
if (mastershipService.isLocalMaster(deviceId)) {
scheduledExecutor.schedule(() -> configCheck(deviceId), 0, TimeUnit.SECONDS);
}
}
private void checkDevices() {
deviceService.getAvailableDevices().forEach(device -> {
triggerConfigCheck(device.id());
});
}
protected static Bmv2DefaultConfiguration loadDefaultConfiguration() {
try {
JsonObject json = Json.parse(new BufferedReader(new InputStreamReader(
Bmv2DeviceContextServiceImpl.class.getResourceAsStream(JSON_DEFAULT_CONFIG_PATH)))).asObject();
return parse(json);
} catch (IOException e) {
throw new RuntimeException("Unable to load default configuration", e);
}
}
/**
* Listener of context changes that immediately triggers config checks (to swap the config if necessary).
*/
private class ContextMapEventListener implements MapEventListener<DeviceId, Bmv2DeviceContext> {
@Override
public void event(MapEvent<DeviceId, Bmv2DeviceContext> event) {
DeviceId deviceId = event.key();
if (event.type().equals(INSERT) || event.type().equals(UPDATE)) {
log.trace("Context {} for {}", event.type().name(), deviceId);
triggerConfigCheck(deviceId);
}
}
}
/**
* Context serializer.
*/
private class BmvDeviceContextSerializer extends com.esotericsoftware.kryo.Serializer<Bmv2DeviceContext> {
@Override
public void write(Kryo kryo, Output output, Bmv2DeviceContext context) {
kryo.writeObject(output, context.configuration().json().toString());
kryo.writeObject(output, context.interpreter().getClass().getName());
}
@Override
public Bmv2DeviceContext read(Kryo kryo, Input input, Class<Bmv2DeviceContext> type) {
String jsonStr = kryo.readObject(input, String.class);
String interpreterClassName = kryo.readObject(input, String.class);
Bmv2Configuration configuration = parse(Json.parse(jsonStr).asObject());
ClassLoader loader = interpreterClassLoaders.get(interpreterClassName);
if (loader == null) {
throw new IllegalStateException("No class loader registered for interpreter: " + interpreterClassName);
}
try {
Bmv2Interpreter interpreter = (Bmv2Interpreter) loader.loadClass(interpreterClassName).newInstance();
return new Bmv2DeviceContext(configuration, interpreter);
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
throw new RuntimeException("Unable to load interpreter class", e);
}
}
}
}