/* * 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); } } } }