blob: 7c46c0bb01539d3764cf1023a0712207e013cb7c [file] [log] [blame]
/*
* Copyright 2015-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.app.impl;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.Uninterruptibles;
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.apache.karaf.features.Feature;
import org.apache.karaf.features.FeaturesService;
import org.onosproject.app.ApplicationAdminService;
import org.onosproject.app.ApplicationEvent;
import org.onosproject.app.ApplicationListener;
import org.onosproject.app.ApplicationService;
import org.onosproject.app.ApplicationState;
import org.onosproject.app.ApplicationStore;
import org.onosproject.app.ApplicationStoreDelegate;
import org.onosproject.core.Application;
import org.onosproject.core.ApplicationId;
import org.onosproject.event.AbstractListenerManager;
import org.onosproject.security.Permission;
import org.onosproject.security.SecurityUtil;
import org.slf4j.Logger;
import java.io.InputStream;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.onosproject.app.ApplicationEvent.Type.APP_ACTIVATED;
import static org.onosproject.app.ApplicationEvent.Type.APP_DEACTIVATED;
import static org.onosproject.app.ApplicationEvent.Type.APP_INSTALLED;
import static org.onosproject.app.ApplicationEvent.Type.APP_UNINSTALLED;
import static org.onosproject.security.AppGuard.checkPermission;
import static org.onosproject.security.AppPermission.Type.APP_READ;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Implementation of the application management service.
*/
@Component(immediate = true)
@Service
public class ApplicationManager
extends AbstractListenerManager<ApplicationEvent, ApplicationListener>
implements ApplicationService, ApplicationAdminService {
private final Logger log = getLogger(getClass());
private static final String APP_ID_NULL = "Application ID cannot be null";
private static final long DEFAULT_OPERATION_TIMEOUT_MILLIS = 2000;
private final ApplicationStoreDelegate delegate = new InternalStoreDelegate();
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected ApplicationStore store;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected FeaturesService featuresService;
// Application supplied hooks for pre-activation processing.
private final Multimap<String, Runnable> deactivateHooks = HashMultimap.create();
private final Cache<ApplicationId, CountDownLatch> pendingOperations =
CacheBuilder.newBuilder()
.expireAfterWrite(DEFAULT_OPERATION_TIMEOUT_MILLIS * 2, TimeUnit.MILLISECONDS)
.build();
@Activate
public void activate() {
eventDispatcher.addSink(ApplicationEvent.class, listenerRegistry);
store.setDelegate(delegate);
log.info("Started");
}
@Deactivate
public void deactivate() {
eventDispatcher.removeSink(ApplicationEvent.class);
store.unsetDelegate(delegate);
log.info("Stopped");
}
@Override
public Set<Application> getApplications() {
checkPermission(APP_READ);
return store.getApplications();
}
@Override
public ApplicationId getId(String name) {
checkPermission(APP_READ);
checkNotNull(name, "Name cannot be null");
return store.getId(name);
}
@Override
public Application getApplication(ApplicationId appId) {
checkPermission(APP_READ);
checkNotNull(appId, APP_ID_NULL);
return store.getApplication(appId);
}
@Override
public ApplicationState getState(ApplicationId appId) {
checkPermission(APP_READ);
checkNotNull(appId, APP_ID_NULL);
return store.getState(appId);
}
@Override
public Set<Permission> getPermissions(ApplicationId appId) {
checkPermission(APP_READ);
checkNotNull(appId, APP_ID_NULL);
return store.getPermissions(appId);
}
@Override
public void registerDeactivateHook(ApplicationId appId, Runnable hook) {
checkPermission(APP_READ);
checkNotNull(appId, APP_ID_NULL);
checkNotNull(hook, "Hook cannot be null");
deactivateHooks.put(appId.name(), hook);
}
@Override
public Application install(InputStream appDescStream) {
checkNotNull(appDescStream, "Application archive stream cannot be null");
Application app = store.create(appDescStream);
SecurityUtil.register(app.id());
return app;
}
@Override
public void uninstall(ApplicationId appId) {
checkNotNull(appId, APP_ID_NULL);
updateStoreAndWaitForNotificationHandling(appId, store::remove);
}
@Override
public void activate(ApplicationId appId) {
checkNotNull(appId, APP_ID_NULL);
if (!SecurityUtil.isAppSecured(appId)) {
return;
}
updateStoreAndWaitForNotificationHandling(appId, store::activate);
}
@Override
public void deactivate(ApplicationId appId) {
checkNotNull(appId, APP_ID_NULL);
updateStoreAndWaitForNotificationHandling(appId, store::deactivate);
}
@Override
public void setPermissions(ApplicationId appId, Set<Permission> permissions) {
checkNotNull(appId, APP_ID_NULL);
checkNotNull(permissions, "Permissions cannot be null");
store.setPermissions(appId, permissions);
}
private void updateStoreAndWaitForNotificationHandling(ApplicationId appId,
Consumer<ApplicationId> storeUpdateTask) {
CountDownLatch latch = new CountDownLatch(1);
try {
pendingOperations.put(appId, latch);
storeUpdateTask.accept(appId);
} catch (Exception e) {
pendingOperations.invalidate(appId);
latch.countDown();
log.warn("Failed to update store for {}", appId.name(), e);
}
Uninterruptibles.awaitUninterruptibly(latch, DEFAULT_OPERATION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
private class InternalStoreDelegate implements ApplicationStoreDelegate {
@Override
public void notify(ApplicationEvent event) {
ApplicationEvent.Type type = event.type();
Application app = event.subject();
CountDownLatch latch = pendingOperations.getIfPresent(app.id());
try {
if (type == APP_ACTIVATED) {
if (installAppFeatures(app)) {
log.info("Application {} has been activated", app.id().name());
}
} else if (type == APP_DEACTIVATED) {
if (uninstallAppFeatures(app)) {
log.info("Application {} has been deactivated", app.id().name());
}
} else if (type == APP_INSTALLED) {
if (installAppArtifacts(app)) {
log.info("Application {} has been installed", app.id().name());
}
} else if (type == APP_UNINSTALLED) {
if (uninstallAppFeatures(app) || uninstallAppArtifacts(app)) {
log.info("Application {} has been uninstalled", app.id().name());
}
}
post(event);
} catch (Exception e) {
log.warn("Unable to perform operation on application " + app.id().name(), e);
} finally {
if (latch != null) {
latch.countDown();
pendingOperations.invalidate(app.id());
}
}
}
}
// The following methods are fully synchronized to guard against remote vs.
// locally induced feature service interactions.
// Installs all feature repositories required by the specified app.
private synchronized boolean installAppArtifacts(Application app) throws Exception {
if (app.featuresRepo().isPresent() &&
featuresService.getRepository(app.featuresRepo().get()) == null) {
featuresService.addRepository(app.featuresRepo().get());
return true;
}
return false;
}
// Uninstalls all the feature repositories required by the specified app.
private synchronized boolean uninstallAppArtifacts(Application app) throws Exception {
if (app.featuresRepo().isPresent() &&
featuresService.getRepository(app.featuresRepo().get()) != null) {
featuresService.removeRepository(app.featuresRepo().get());
return true;
}
return false;
}
// Installs all features that define the specified app.
private synchronized boolean installAppFeatures(Application app) throws Exception {
boolean changed = false;
for (String name : app.features()) {
Feature feature = featuresService.getFeature(name);
// If we see an attempt at activation of a non-existent feature
// attempt to install the app artifacts first and then retry.
// This can be triggered by a race condition between different ONOS
// instances "installing" the apps from disk at their own pace.
// Perhaps there is a more elegant solution to be explored in the
// future.
if (feature == null) {
installAppArtifacts(app);
feature = featuresService.getFeature(name);
}
if (feature != null && !featuresService.isInstalled(feature)) {
featuresService.installFeature(name);
changed = true;
} else if (feature == null) {
log.warn("Feature {} not found", name);
}
}
return changed;
}
// Uninstalls all features that define the specified app.
private synchronized boolean uninstallAppFeatures(Application app) throws Exception {
boolean changed = false;
deactivateHooks.removeAll(app.id().name()).forEach(hook -> invokeHook(hook, app.id()));
for (String name : app.features()) {
Feature feature = featuresService.getFeature(name);
if (feature != null && featuresService.isInstalled(feature)) {
featuresService.uninstallFeature(name);
changed = true;
} else if (feature == null) {
log.warn("Feature {} not found", name);
}
}
return changed;
}
// Invokes the specified function, if not null.
@java.lang.SuppressWarnings("squid:S1217") // We really do mean to call run()
private void invokeHook(Runnable hook, ApplicationId appId) {
if (hook != null) {
try {
hook.run();
} catch (Exception e) {
log.warn("Deactivate hook for application {} encountered an error",
appId.name(), e);
}
}
}
}