http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/BrooklynObjectManagerInternal.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/BrooklynObjectManagerInternal.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/BrooklynObjectManagerInternal.java new file mode 100644 index 0000000..d22ffae --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/BrooklynObjectManagerInternal.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +import org.apache.brooklyn.api.basic.BrooklynObject; + +public interface BrooklynObjectManagerInternal<T extends BrooklynObject> { + + ManagementTransitionMode getLastManagementTransitionMode(String itemId); + void setManagementTransitionMode(T item, ManagementTransitionMode mode); + + /** + * Begins management for the given rebinded root, recursively; + * if rebinding as a read-only copy, {@link #setReadOnly(T, boolean)} should be called prior to this. + */ + void manageRebindedRoot(T item); + + void unmanage(final T e, final ManagementTransitionMode info); + +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/CollectionChangeListener.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/CollectionChangeListener.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/CollectionChangeListener.java new file mode 100644 index 0000000..45fa765 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/CollectionChangeListener.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +public interface CollectionChangeListener<Item> { + void onItemAdded(Item item); + void onItemRemoved(Item item); +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/EffectorUtils.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/EffectorUtils.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/EffectorUtils.java new file mode 100644 index 0000000..028f2d2 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/EffectorUtils.java @@ -0,0 +1,356 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +import static brooklyn.util.GroovyJavaMethods.truth; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.apache.brooklyn.api.entity.Effector; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.ParameterType; +import org.apache.brooklyn.api.management.Task; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.entity.basic.BasicParameterType; +import brooklyn.entity.basic.BrooklynTaskTags; +import brooklyn.entity.basic.EntityInternal; +import brooklyn.util.collections.MutableList; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.exceptions.PropagatedRuntimeException; +import brooklyn.util.flags.TypeCoercions; +import brooklyn.util.guava.Maybe; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +/** + * Utility methods for invoking effectors. + */ +public class EffectorUtils { + + private static final Logger log = LoggerFactory.getLogger(EffectorUtils.class); + + /** prepares arguments for an effector either accepting: + * an array, which should contain the arguments in order, optionally omitting those which have defaults defined; + * or a map, which should contain the arguments by name, again optionally omitting those which have defaults defined, + * and in this case also performing type coercion. + */ + public static Object[] prepareArgsForEffector(Effector<?> eff, Object args) { + if (args != null && args.getClass().isArray()) { + return prepareArgsForEffectorFromArray(eff, (Object[]) args); + } + if (args instanceof Map) { + return prepareArgsForEffectorFromMap(eff, (Map) args); + } + log.warn("Deprecated effector invocation style for call to "+eff+", expecting a map or an array, got: "+args); + if (log.isDebugEnabled()) { + log.debug("Deprecated effector invocation style for call to "+eff+", expecting a map or an array, got: "+args, + new Throwable("Trace for deprecated effector invocation style")); + } + return oldPrepareArgsForEffector(eff, args); + } + + /** method used for calls such as entity.effector(arg1, arg2) + * get routed here from AbstractEntity.invokeMethod */ + private static Object[] prepareArgsForEffectorFromArray(Effector<?> eff, Object args[]) { + int newArgsNeeded = eff.getParameters().size(); + if (args.length==1 && args[0] instanceof Map) { + if (newArgsNeeded!=1 || !eff.getParameters().get(0).getParameterClass().isAssignableFrom(args[0].getClass())) { + // treat a map in an array as a map passed directly (unless the method takes a single-arg map) + // this is to support effector(param1: val1) + return prepareArgsForEffectorFromMap(eff, (Map) args[0]); + } + } + return prepareArgsForEffectorAsMapFromArray(eff, args).values().toArray(new Object[0]); + } + + public static Map prepareArgsForEffectorAsMapFromArray(Effector<?> eff, Object args[]) { + int newArgsNeeded = eff.getParameters().size(); + List l = Lists.newArrayList(); + l.addAll(Arrays.asList(args)); + Map newArgs = new LinkedHashMap(); + + for (int index = 0; index < eff.getParameters().size(); index++) { + ParameterType<?> it = eff.getParameters().get(index); + + if (l.size() >= newArgsNeeded) { + //all supplied (unnamed) arguments must be used; ignore map + newArgs.put(it.getName(), l.remove(0)); + // TODO do we ignore arguments in the same order that groovy does? + } else if (!l.isEmpty() && it.getParameterClass().isInstance(l.get(0))) { + //if there are parameters supplied, and type is correct, they get applied before default values + //(this is akin to groovy) + newArgs.put(it.getName(), l.remove(0)); + } else if (it instanceof BasicParameterType && ((BasicParameterType)it).hasDefaultValue()) { + //finally, default values are used to make up for missing parameters + newArgs.put(it.getName(), ((BasicParameterType)it).getDefaultValue()); + } else { + throw new IllegalArgumentException("Invalid arguments (count mismatch) for effector "+eff+": "+args); + } + + newArgsNeeded--; + } + if (newArgsNeeded > 0) { + throw new IllegalArgumentException("Invalid arguments (missing "+newArgsNeeded+") for effector "+eff+": "+args); + } + if (!l.isEmpty()) { + throw new IllegalArgumentException("Invalid arguments ("+l.size()+" extra) for effector "+eff+": "+args); + } + return newArgs; + } + + private static Object[] prepareArgsForEffectorFromMap(Effector<?> eff, Map m) { + m = Maps.newLinkedHashMap(m); //make editable copy + List newArgs = Lists.newArrayList(); + int newArgsNeeded = eff.getParameters().size(); + + for (int index = 0; index < eff.getParameters().size(); index++) { + ParameterType<?> it = eff.getParameters().get(index); + Object v; + if (truth(it.getName()) && m.containsKey(it.getName())) { + // argument is in the map + v = m.remove(it.getName()); + } else if (it instanceof BasicParameterType && ((BasicParameterType)it).hasDefaultValue()) { + //finally, default values are used to make up for missing parameters + v = ((BasicParameterType)it).getDefaultValue(); + } else { + throw new IllegalArgumentException("Invalid arguments (missing argument "+it+") for effector "+eff+": "+m); + } + + newArgs.add(TypeCoercions.coerce(v, it.getParameterClass())); + newArgsNeeded--; + } + if (newArgsNeeded>0) + throw new IllegalArgumentException("Invalid arguments (missing "+newArgsNeeded+") for effector "+eff+": "+m); + return newArgs.toArray(new Object[newArgs.size()]); + } + + /** + * Takes arguments, and returns an array of arguments suitable for use by the Effector + * according to the ParameterTypes it exposes. + * <p> + * The args can be: + * <ol> + * <li>an array of ordered arguments + * <li>a collection (which will be automatically converted to an array) + * <li>a single argument (which will then be wrapped in an array) + * <li>a map containing the (named) arguments + * <li>an array or collection single entry of a map (treated same as 5 above) + * <li>a semi-populated array or collection that also containing a map as first arg - + * uses ordered args in array, but uses named values from map in preference. + * <li>semi-populated array or collection, where default values will otherwise be used. + * </ol> + */ + public static Object[] oldPrepareArgsForEffector(Effector<?> eff, Object args) { + //attempt to coerce unexpected types + Object[] argsArray; + if (args==null) { + argsArray = new Object[0]; + } else if (args.getClass().isArray()) { + argsArray = (Object[]) args; + } else { + if (args instanceof Collection) { + argsArray = ((Collection) args).toArray(new Object[((Collection) args).size()]); + } else { + argsArray = new Object[] { args }; + } + } + + //if args starts with a map, assume it contains the named arguments + //(but only use it when we have insufficient supplied arguments) + List l = Lists.newArrayList(); + l.addAll(Arrays.asList(argsArray)); + Map m = (argsArray.length > 0 && argsArray[0] instanceof Map ? Maps.newLinkedHashMap((Map) l.remove(0)) : null); + List newArgs = Lists.newArrayList(); + int newArgsNeeded = eff.getParameters().size(); + boolean mapUsed = false; + + for (int index = 0; index < eff.getParameters().size(); index++) { + ParameterType<?> it = eff.getParameters().get(index); + + if (l.size() >= newArgsNeeded) { + //all supplied (unnamed) arguments must be used; ignore map + newArgs.add(l.remove(0)); + } else if (truth(m) && truth(it.getName()) && m.containsKey(it.getName())) { + //some arguments were not supplied, and this one is in the map + newArgs.add(m.remove(it.getName())); + } else if (index == 0 && Map.class.isAssignableFrom(it.getParameterClass())) { + //if first arg is a map it takes the supplied map + newArgs.add(m); + mapUsed = true; + } else if (!l.isEmpty() && it.getParameterClass().isInstance(l.get(0))) { + //if there are parameters supplied, and type is correct, they get applied before default values + //(this is akin to groovy) + newArgs.add(l.remove(0)); + } else if (it instanceof BasicParameterType && ((BasicParameterType)it).hasDefaultValue()) { + //finally, default values are used to make up for missing parameters + newArgs.add(((BasicParameterType)it).getDefaultValue()); + } else { + throw new IllegalArgumentException("Invalid arguments (count mismatch) for effector "+eff+": "+args); + } + + newArgsNeeded--; + } + if (newArgsNeeded > 0) { + throw new IllegalArgumentException("Invalid arguments (missing "+newArgsNeeded+") for effector "+eff+": "+args); + } + if (!l.isEmpty()) { + throw new IllegalArgumentException("Invalid arguments ("+l.size()+" extra) for effector "+eff+": "+args); + } + if (truth(m) && !mapUsed) { + throw new IllegalArgumentException("Invalid arguments ("+m.size()+" extra named) for effector "+eff+": "+args); + } + return newArgs.toArray(new Object[newArgs.size()]); + } + + /** + * Invokes a method effector so that its progress is tracked. For internal use only, when we know the effector is backed by a method which is local. + */ + public static <T> T invokeMethodEffector(Entity entity, Effector<T> eff, Object[] args) { + String name = eff.getName(); + + try { + if (log.isDebugEnabled()) log.debug("Invoking effector {} on {}", new Object[] {name, entity}); + if (log.isTraceEnabled()) log.trace("Invoking effector {} on {} with args {}", new Object[] {name, entity, args}); + EntityManagementSupport mgmtSupport = ((EntityInternal)entity).getManagementSupport(); + if (!mgmtSupport.isDeployed()) { + mgmtSupport.attemptLegacyAutodeployment(name); + } + ManagementContextInternal mgmtContext = (ManagementContextInternal) ((EntityInternal) entity).getManagementContext(); + + mgmtSupport.getEntityChangeListener().onEffectorStarting(eff, args); + try { + return mgmtContext.invokeEffectorMethodSync(entity, eff, args); + } finally { + mgmtSupport.getEntityChangeListener().onEffectorCompleted(eff); + } + } catch (Exception e) { + handleEffectorException(entity, eff, e); + // (won't return below) + return null; + } + } + + public static void handleEffectorException(Entity entity, Effector<?> effector, Throwable throwable) { + String message = "Error invoking " + effector.getName() + " at " + entity; + // Avoid throwing a PropagatedRuntimeException that just repeats the last PropagatedRuntimeException. + if (throwable instanceof PropagatedRuntimeException && + throwable.getMessage() != null && + throwable.getMessage().startsWith(message)) { + throw PropagatedRuntimeException.class.cast(throwable); + } else { + log.warn(message + ": " + Exceptions.collapseText(throwable)); + throw new PropagatedRuntimeException(message, throwable); + } + } + + public static <T> Task<T> invokeEffectorAsync(Entity entity, Effector<T> eff, Map<String,?> parameters) { + String name = eff.getName(); + + if (log.isDebugEnabled()) log.debug("Invoking-async effector {} on {}", new Object[] { name, entity }); + if (log.isTraceEnabled()) log.trace("Invoking-async effector {} on {} with args {}", new Object[] { name, entity, parameters }); + EntityManagementSupport mgmtSupport = ((EntityInternal)entity).getManagementSupport(); + if (!mgmtSupport.isDeployed()) { + mgmtSupport.attemptLegacyAutodeployment(name); + } + ManagementContextInternal mgmtContext = (ManagementContextInternal) ((EntityInternal)entity).getManagementContext(); + + // FIXME seems brittle to have the listeners in the Utils method; better to move into the context.invokeEff + // (or whatever the last mile before invoking the effector is - though currently there is not such a canonical place!) + mgmtSupport.getEntityChangeListener().onEffectorStarting(eff, parameters); + try { + return mgmtContext.invokeEffector(entity, eff, parameters); + } finally { + // FIXME this is really Effector submitted + mgmtSupport.getEntityChangeListener().onEffectorCompleted(eff); + } + } + + /** @deprecated since 0.7.0, not used */ + @Deprecated + public static Effector<?> findEffectorMatching(Entity entity, Method method) { + outer: for (Effector<?> effector : entity.getEntityType().getEffectors()) { + if (!effector.getName().equals(entity)) continue; + if (effector.getParameters().size() != method.getParameterTypes().length) continue; + for (int i = 0; i < effector.getParameters().size(); i++) { + if (effector.getParameters().get(i).getParameterClass() != method.getParameterTypes()[i]) continue outer; + } + return effector; + } + return null; + } + + /** @deprecated since 0.7.0, expects parameters but does not use them! */ + @Deprecated + public static Effector<?> findEffectorMatching(Set<Effector<?>> effectors, String effectorName, Map<String, ?> parameters) { + // TODO Support overloading: check parameters as well + for (Effector<?> effector : effectors) { + if (effector.getName().equals(effectorName)) { + return effector; + } + } + return null; + } + + /** matches effectors by name only (not parameters) */ + public static Maybe<Effector<?>> findEffector(Collection<? extends Effector<?>> effectors, String effectorName) { + for (Effector<?> effector : effectors) { + if (effector.getName().equals(effectorName)) { + return Maybe.<Effector<?>>of(effector); + } + } + return Maybe.absent(new NoSuchElementException("No effector with name "+effectorName+" (contenders "+effectors+")")); + } + + /** matches effectors by name only (not parameters), based on what is declared on the entity static type */ + public static Maybe<Effector<?>> findEffectorDeclared(Entity entity, String effectorName) { + return findEffector(entity.getEntityType().getEffectors(), effectorName); + } + + /** @deprecated since 0.7.0 use {@link #getTaskFlagsForEffectorInvocation(Entity, Effector, ConfigBag)} */ + public static Map<Object,Object> getTaskFlagsForEffectorInvocation(Entity entity, Effector<?> effector) { + return getTaskFlagsForEffectorInvocation(entity, effector, null); + } + + /** returns a (mutable) map of the standard flags which should be placed on an effector */ + public static Map<Object,Object> getTaskFlagsForEffectorInvocation(Entity entity, Effector<?> effector, ConfigBag parameters) { + return MutableMap.builder() + .put("description", "Invoking effector "+effector.getName() + +" on "+entity.getDisplayName() + +(parameters!=null ? " with parameters "+parameters.getAllConfig() : "")) + .put("displayName", effector.getName()) + .put("tags", MutableList.of( + BrooklynTaskTags.EFFECTOR_TAG, + BrooklynTaskTags.tagForEffectorCall(entity, effector.getName(), parameters), + BrooklynTaskTags.tagForTargetEntity(entity))) + .build(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityChangeListener.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityChangeListener.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityChangeListener.java new file mode 100644 index 0000000..d6552ff --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityChangeListener.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +import org.apache.brooklyn.api.entity.Effector; +import org.apache.brooklyn.api.entity.Feed; +import org.apache.brooklyn.api.event.AttributeSensor; +import org.apache.brooklyn.api.policy.Enricher; +import org.apache.brooklyn.api.policy.Policy; + +import brooklyn.config.ConfigKey; + +public interface EntityChangeListener { + + // TODO for testing only! + public static final EntityChangeListener NOOP = new EntityChangeListener() { + @Override public void onChanged() {} + @Override public void onAttributeChanged(AttributeSensor<?> attribute) {} + @Override public void onConfigChanged(ConfigKey<?> key) {} + @Override public void onLocationsChanged() {} + @Override public void onMembersChanged() {} + @Override public void onTagsChanged() {} + @Override public void onChildrenChanged() {} + @Override public void onPolicyAdded(Policy policy) {} + @Override public void onPolicyRemoved(Policy policy) {} + @Override public void onEnricherAdded(Enricher enricher) {} + @Override public void onEnricherRemoved(Enricher enricher) {} + @Override public void onFeedAdded(Feed feed) {} + @Override public void onFeedRemoved(Feed feed) {} + @Override public void onEffectorStarting(Effector<?> effector, Object parameters) {} + @Override public void onEffectorCompleted(Effector<?> effector) {} + }; + + void onChanged(); + + void onAttributeChanged(AttributeSensor<?> attribute); + + void onConfigChanged(ConfigKey<?> key); + + void onLocationsChanged(); + + void onTagsChanged(); + + void onMembersChanged(); + + void onChildrenChanged(); + + void onPolicyAdded(Policy policy); + + void onPolicyRemoved(Policy policy); + + void onEnricherAdded(Enricher enricher); + + void onEnricherRemoved(Enricher enricher); + + void onFeedAdded(Feed feed); + + void onFeedRemoved(Feed feed); + + void onEffectorStarting(Effector<?> effector, Object parameters); + + void onEffectorCompleted(Effector<?> effector); +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagementSupport.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagementSupport.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagementSupport.java new file mode 100644 index 0000000..dbba29c --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagementSupport.java @@ -0,0 +1,478 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.brooklyn.api.entity.Application; +import org.apache.brooklyn.api.entity.Effector; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.Feed; +import org.apache.brooklyn.api.event.AttributeSensor; +import org.apache.brooklyn.api.management.ExecutionContext; +import org.apache.brooklyn.api.management.ManagementContext; +import org.apache.brooklyn.api.management.SubscriptionContext; +import org.apache.brooklyn.api.management.entitlement.EntitlementManager; +import org.apache.brooklyn.api.policy.Enricher; +import org.apache.brooklyn.api.policy.Policy; +import org.apache.brooklyn.core.management.entitlement.Entitlements; +import org.apache.brooklyn.core.management.entitlement.Entitlements.EntityAndItem; +import org.apache.brooklyn.core.management.entitlement.Entitlements.StringAndArgument; +import org.apache.brooklyn.core.management.internal.NonDeploymentManagementContext.NonDeploymentManagementContextMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.AbstractEntity; +import brooklyn.entity.basic.Entities; +import brooklyn.entity.basic.EntityInternal; +import brooklyn.util.exceptions.Exceptions; + +import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; + +/** + * Encapsulates management activities at an entity. + * <p> + * On entity deployment, ManagementContext.manage(entity) causes + * <p> + * * onManagementStarting(ManagementContext) + * * onManagementStartingSubscriptions() + * * onManagementStartingSensorEmissions() + * * onManagementStartingExecutions() + * * onManagementStarted() - when all the above is said and done + * * onManagementStartingHere(); + * <p> + * on unmanage it hits onManagementStoppingHere() then onManagementStopping(). + * <p> + * When an entity's management migrates, it invoked onManagementStoppingHere() at the old location, + * then onManagementStartingHere() at the new location. + */ +public class EntityManagementSupport { + + private static final Logger log = LoggerFactory.getLogger(EntityManagementSupport.class); + + public EntityManagementSupport(AbstractEntity entity) { + this.entity = entity; + nonDeploymentManagementContext = new NonDeploymentManagementContext(entity, NonDeploymentManagementContextMode.PRE_MANAGEMENT); + } + + protected transient AbstractEntity entity; + NonDeploymentManagementContext nonDeploymentManagementContext; + + protected transient ManagementContext initialManagementContext; + protected transient ManagementContext managementContext; + protected transient SubscriptionContext subscriptionContext; + protected transient ExecutionContext executionContext; + + // TODO the application + // (elaborate or remove ^^^ ? -AH, Sept 2014) + + protected final AtomicBoolean managementContextUsable = new AtomicBoolean(false); + protected final AtomicBoolean currentlyDeployed = new AtomicBoolean(false); + protected final AtomicBoolean everDeployed = new AtomicBoolean(false); + protected Boolean readOnly = null; + protected final AtomicBoolean managementFailed = new AtomicBoolean(false); + + private volatile EntityChangeListener entityChangeListener = EntityChangeListener.NOOP; + + /** + * Whether this entity is managed (i.e. "onManagementStarting" has been called, so the framework knows about it, + * and it has not been unmanaged). + */ + public boolean isDeployed() { return currentlyDeployed.get(); } + public boolean isNoLongerManaged() { + return wasDeployed() && !isDeployed(); + } + /** whether entity has ever been deployed (managed) */ + public boolean wasDeployed() { return everDeployed.get(); } + + @Beta + public void setReadOnly(boolean isReadOnly) { + if (isDeployed()) + throw new IllegalStateException("Cannot set read only after deployment"); + this.readOnly = isReadOnly; + } + + /** Whether the entity and its adjuncts should be treated as read-only; + * may be null briefly when initializing if RO status is unknown. */ + @Beta + public Boolean isReadOnlyRaw() { + return readOnly; + } + + /** Whether the entity and its adjuncts should be treated as read-only; + * error if initializing and RO status is unknown. */ + @Beta + public boolean isReadOnly() { + Preconditions.checkNotNull(readOnly, "Read-only status of %s not yet known", entity); + return readOnly; + } + + /** + * Whether the entity's management lifecycle is complete (i.e. both "onManagementStarting" and "onManagementStarted" have + * been called, and it is has not been unmanaged). + */ + public boolean isFullyManaged() { + return (nonDeploymentManagementContext == null) && currentlyDeployed.get(); + } + + public synchronized void setManagementContext(ManagementContextInternal val) { + if (initialManagementContext != null) { + throw new IllegalStateException("Initial management context is already set for "+entity+"; cannot change"); + } + if (managementContext != null && !managementContext.equals(val)) { + throw new IllegalStateException("Management context is already set for "+entity+"; cannot change"); + } + + this.initialManagementContext = checkNotNull(val, "managementContext"); + if (nonDeploymentManagementContext != null) { + nonDeploymentManagementContext.setManagementContext(val); + } + } + + public void onRebind(ManagementTransitionInfo info) { + nonDeploymentManagementContext.setMode(NonDeploymentManagementContextMode.MANAGEMENT_REBINDING); + } + + public void onManagementStarting(ManagementTransitionInfo info) { + try { + synchronized (this) { + boolean alreadyManaging = isDeployed(); + + if (alreadyManaging) { + log.warn("Already managed: "+entity+" ("+nonDeploymentManagementContext+"); onManagementStarting is no-op"); + } else if (nonDeploymentManagementContext == null || !nonDeploymentManagementContext.getMode().isPreManaged()) { + throw new IllegalStateException("Not in expected pre-managed state: "+entity+" ("+nonDeploymentManagementContext+")"); + } + if (managementContext != null && !managementContext.equals(info.getManagementContext())) { + throw new IllegalStateException("Already has management context: "+managementContext+"; can't set "+info.getManagementContext()); + } + if (initialManagementContext != null && !initialManagementContext.equals(info.getManagementContext())) { + throw new IllegalStateException("Already has different initial management context: "+initialManagementContext+"; can't set "+info.getManagementContext()); + } + if (alreadyManaging) { + return; + } + + this.managementContext = info.getManagementContext(); + nonDeploymentManagementContext.setMode(NonDeploymentManagementContextMode.MANAGEMENT_STARTING); + + if (!isReadOnly()) { + nonDeploymentManagementContext.getSubscriptionManager().setDelegate((AbstractSubscriptionManager) managementContext.getSubscriptionManager()); + nonDeploymentManagementContext.getSubscriptionManager().startDelegatingForSubscribing(); + } + + managementContextUsable.set(true); + currentlyDeployed.set(true); + everDeployed.set(true); + + entityChangeListener = new EntityChangeListenerImpl(); + } + + /* + * TODO framework starting events - phase 1, including rebind + * - establish hierarchy (child, groups, etc; construction if necessary on rebind) + * - set location + * - set local config values + * - set saved sensor values + * - register subscriptions -- BUT nothing is allowed to execute + * [these operations may be done before we invoke starting also; above can happen in any order; + * sensor _publications_ and executor submissions are queued] + * then: set the management context and the entity is "managed" from the perspective of external viewers (ManagementContext.isManaged(entity) returns true) + */ + + if (!isReadOnly()) { + entity.onManagementStarting(); + } + } catch (Throwable t) { + managementFailed.set(true); + throw Exceptions.propagate(t); + } + } + + @SuppressWarnings("deprecation") + public void onManagementStarted(ManagementTransitionInfo info) { + try { + synchronized (this) { + boolean alreadyManaged = isFullyManaged(); + + if (alreadyManaged) { + log.warn("Already managed: "+entity+" ("+nonDeploymentManagementContext+"); onManagementStarted is no-op"); + } else if (nonDeploymentManagementContext == null || nonDeploymentManagementContext.getMode() != NonDeploymentManagementContextMode.MANAGEMENT_STARTING) { + throw new IllegalStateException("Not in expected \"management starting\" state: "+entity+" ("+nonDeploymentManagementContext+")"); + } + if (managementContext != info.getManagementContext()) { + throw new IllegalStateException("Already has management context: "+managementContext+"; can't set "+info.getManagementContext()); + } + if (alreadyManaged) { + return; + } + + nonDeploymentManagementContext.setMode(NonDeploymentManagementContextMode.MANAGEMENT_STARTED); + + /* + * - set derived/inherited config values + * - publish all queued sensors + * - start all queued executions (e.g. subscription delivery) + * [above happens in exactly this order, at each entity] + * then: the entity internally knows it fully managed (ManagementSupport.isManaged() returns true -- though not sure we need that); + * subsequent sensor events and executions occur directly (no queueing) + */ + + if (!isReadOnly()) { + nonDeploymentManagementContext.getSubscriptionManager().startDelegatingForPublishing(); + } + + // TODO more of the above + // TODO custom started activities + // (elaborate or remove ^^^ ? -AH, Sept 2014) + } + + if (!isReadOnly()) { + entity.onManagementBecomingMaster(); + entity.onManagementStarted(); + } + + synchronized (this) { + nonDeploymentManagementContext = null; + } + } catch (Throwable t) { + managementFailed.set(true); + throw Exceptions.propagate(t); + } + } + + @SuppressWarnings("deprecation") + public void onManagementStopping(ManagementTransitionInfo info) { + synchronized (this) { + if (managementContext != info.getManagementContext()) { + throw new IllegalStateException("onManagementStopping encountered different management context for "+entity+ + (!wasDeployed() ? " (wasn't deployed)" : !isDeployed() ? " (no longer deployed)" : "")+ + ": "+managementContext+"; expected "+info.getManagementContext()+" (may be a pre-registered entity which was never properly managed)"); + } + Stopwatch startTime = Stopwatch.createStarted(); + while (!managementFailed.get() && nonDeploymentManagementContext!=null && + nonDeploymentManagementContext.getMode()==NonDeploymentManagementContextMode.MANAGEMENT_STARTING) { + // still becoming managed + try { + if (startTime.elapsed(TimeUnit.SECONDS) > 30) { + // emergency fix, 30s timeout for management starting + log.error("Management stopping event "+info+" in "+this+" timed out waiting for start; proceeding to stopping"); + break; + } + wait(100); + } catch (InterruptedException e) { + Exceptions.propagate(e); + } + } + if (nonDeploymentManagementContext==null) { + nonDeploymentManagementContext = new NonDeploymentManagementContext(entity, NonDeploymentManagementContextMode.MANAGEMENT_STOPPING); + } else { + // already stopped? or not started? + nonDeploymentManagementContext.setMode(NonDeploymentManagementContextMode.MANAGEMENT_STOPPING); + } + } + // TODO custom stopping activities + // TODO framework stopping events - no more sensors, executions, etc + // (elaborate or remove ^^^ ? -AH, Sept 2014) + + if (!isReadOnly() && info.getMode().isDestroying()) { + // if we support remote parent of local child, the following call will need to be properly remoted + if (entity.getParent()!=null) entity.getParent().removeChild(entity.getProxyIfAvailable()); + } + // new subscriptions will be queued / not allowed + nonDeploymentManagementContext.getSubscriptionManager().stopDelegatingForSubscribing(); + // new publications will be queued / not allowed + nonDeploymentManagementContext.getSubscriptionManager().stopDelegatingForPublishing(); + + if (!isReadOnly()) { + entity.onManagementNoLongerMaster(); + entity.onManagementStopped(); + } + } + + public void onManagementStopped(ManagementTransitionInfo info) { + synchronized (this) { + if (managementContext == null && nonDeploymentManagementContext.getMode() == NonDeploymentManagementContextMode.MANAGEMENT_STOPPED) { + return; + } + if (managementContext != info.getManagementContext()) { + throw new IllegalStateException("Has different management context: "+managementContext+"; expected "+info.getManagementContext()); + } + getSubscriptionContext().unsubscribeAll(); + entityChangeListener = EntityChangeListener.NOOP; + managementContextUsable.set(false); + currentlyDeployed.set(false); + executionContext = null; + subscriptionContext = null; + } + + // TODO framework stopped activities, e.g. serialize state ? + entity.invalidateReferences(); + + synchronized (this) { + managementContext = null; + nonDeploymentManagementContext.setMode(NonDeploymentManagementContextMode.MANAGEMENT_STOPPED); + } + } + + @VisibleForTesting + @Beta + public boolean isManagementContextReal() { + return managementContextUsable.get(); + } + + public synchronized ManagementContext getManagementContext() { + return (managementContextUsable.get()) ? managementContext : nonDeploymentManagementContext; + } + + public synchronized ExecutionContext getExecutionContext() { + if (executionContext!=null) return executionContext; + if (managementContextUsable.get()) { + executionContext = managementContext.getExecutionContext(entity); + return executionContext; + } + return nonDeploymentManagementContext.getExecutionContext(entity); + } + public synchronized SubscriptionContext getSubscriptionContext() { + if (subscriptionContext!=null) return subscriptionContext; + if (managementContextUsable.get()) { + subscriptionContext = managementContext.getSubscriptionContext(entity); + return subscriptionContext; + } + return nonDeploymentManagementContext.getSubscriptionContext(entity); + } + public synchronized EntitlementManager getEntitlementManager() { + return getManagementContext().getEntitlementManager(); + } + + public void attemptLegacyAutodeployment(String effectorName) { + synchronized (this) { + if (managementContext != null) { + log.warn("Autodeployment suggested but not required for " + entity + "." + effectorName); + return; + } + if (entity instanceof Application) { + log.warn("Autodeployment with new management context triggered for " + entity + "." + effectorName + " -- will not be supported in future. Explicit manage call required."); + if (initialManagementContext != null) { + initialManagementContext.getEntityManager().manage(entity); + } else { + Entities.startManagement(entity); + } + return; + } + } + if ("start".equals(effectorName)) { + Entity e=entity; + if (e.getParent()!=null && ((EntityInternal)e.getParent()).getManagementSupport().isDeployed()) { + log.warn("Autodeployment in parent's management context triggered for "+entity+"."+effectorName+" -- will not be supported in future. Explicit manage call required."); + ((EntityInternal)e.getParent()).getManagementContext().getEntityManager().manage(entity); + return; + } + } + log.warn("Autodeployment not available for "+entity+"."+effectorName); + } + + public EntityChangeListener getEntityChangeListener() { + return entityChangeListener; + } + + private class EntityChangeListenerImpl implements EntityChangeListener { + @Override + public void onChanged() { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + } + @Override + public void onChildrenChanged() { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + } + @Override + public void onLocationsChanged() { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + } + @Override + public void onTagsChanged() { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + } + @Override + public void onMembersChanged() { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + } + @Override + public void onPolicyAdded(Policy policy) { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + getManagementContext().getRebindManager().getChangeListener().onManaged(policy); + } + @Override + public void onEnricherAdded(Enricher enricher) { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + getManagementContext().getRebindManager().getChangeListener().onManaged(enricher); + } + @Override + public void onFeedAdded(Feed feed) { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + getManagementContext().getRebindManager().getChangeListener().onManaged(feed); + } + @Override + public void onPolicyRemoved(Policy policy) { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + getManagementContext().getRebindManager().getChangeListener().onUnmanaged(policy); + } + @Override + public void onEnricherRemoved(Enricher enricher) { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + getManagementContext().getRebindManager().getChangeListener().onUnmanaged(enricher); + } + @Override + public void onFeedRemoved(Feed feed) { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + getManagementContext().getRebindManager().getChangeListener().onUnmanaged(feed); + } + @Override + public void onAttributeChanged(AttributeSensor<?> attribute) { + // TODO Could make this more efficient by inspecting the attribute to decide if needs persisted + // immediately, or not important, or transient (e.g. do we really need to persist + // request-per-second count for rebind purposes?!) + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + } + @Override + public void onConfigChanged(ConfigKey<?> key) { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + } + @Override + public void onEffectorStarting(Effector<?> effector, Object parameters) { + Entitlements.checkEntitled(getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, EntityAndItem.of(entity, StringAndArgument.of(effector.getName(), parameters))); + } + @Override + public void onEffectorCompleted(Effector<?> effector) { + getManagementContext().getRebindManager().getChangeListener().onChanged(entity); + } + } + + @Override + public String toString() { + return super.toString()+"["+(entity==null ? "null" : entity.getId())+"]"; + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagementUtils.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagementUtils.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagementUtils.java new file mode 100644 index 0000000..3f59774 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagementUtils.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +import io.brooklyn.camp.CampPlatform; +import io.brooklyn.camp.spi.Assembly; +import io.brooklyn.camp.spi.AssemblyTemplate; +import io.brooklyn.camp.spi.instantiate.AssemblyTemplateInstantiator; + +import java.io.StringReader; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.api.entity.Application; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.basic.EntityLocal; +import org.apache.brooklyn.api.entity.proxying.EntitySpec; +import org.apache.brooklyn.api.management.ManagementContext; +import org.apache.brooklyn.api.management.Task; +import org.apache.brooklyn.api.management.classloading.BrooklynClassLoadingContext; +import org.apache.brooklyn.core.management.classloading.JavaBrooklynClassLoadingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.camp.brooklyn.api.AssemblyTemplateSpecInstantiator; +import brooklyn.config.BrooklynServerConfig; +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.BrooklynTaskTags; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.entity.basic.Entities; +import brooklyn.entity.basic.EntityFunctions; +import brooklyn.entity.effector.Effectors; +import brooklyn.entity.trait.Startable; +import brooklyn.util.collections.MutableList; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.guava.Maybe; +import brooklyn.util.task.TaskBuilder; +import brooklyn.util.task.Tasks; +import brooklyn.util.text.Strings; +import brooklyn.util.time.Duration; + +import com.google.common.annotations.Beta; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +/** Utility methods for working with entities and applications */ +public class EntityManagementUtils { + + private static final Logger log = LoggerFactory.getLogger(EntityManagementUtils.class); + + /** + * A marker config value which indicates that an application was created automatically + * to allow the management of a non-app entity. + */ + public static final ConfigKey<Boolean> WRAPPER_APP_MARKER = ConfigKeys.newBooleanConfigKey("brooklyn.wrapper_app"); + + /** creates an application from the given app spec, managed by the given management context */ + public static <T extends Application> T createUnstarted(ManagementContext mgmt, EntitySpec<T> spec) { + T app = mgmt.getEntityManager().createEntity(spec); + Entities.startManagement(app, mgmt); + return app; + } + + /** convenience for accessing camp */ + public static Maybe<CampPlatform> getCampPlatform(ManagementContext mgmt) { + return BrooklynServerConfig.getCampPlatform(mgmt); + } + + /** as {@link #createApplication(ManagementContext, EntitySpec)} but for a YAML spec */ + public static <T extends Application> T createUnstarted(ManagementContext mgmt, String yaml) { + AssemblyTemplate at = getCampPlatform(mgmt).get().pdp().registerDeploymentPlan( new StringReader(yaml) ); + return createUnstarted(mgmt, at); + } + + /** as {@link #createApplication(ManagementContext, EntitySpec)} but for an assembly template */ + @SuppressWarnings("unchecked") + public static <T extends Application> T createUnstarted(ManagementContext mgmt, AssemblyTemplate at) { + CampPlatform camp = getCampPlatform(mgmt).get(); + AssemblyTemplateInstantiator instantiator; + try { + instantiator = at.getInstantiator().newInstance(); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + Assembly assembly; + if (instantiator instanceof AssemblyTemplateSpecInstantiator) { + BrooklynClassLoadingContext loader = JavaBrooklynClassLoadingContext.create(mgmt); + + EntitySpec<?> spec = ((AssemblyTemplateSpecInstantiator) instantiator).createSpec(at, camp, loader, true); + Entity app = mgmt.getEntityManager().createEntity(spec); + Entities.startManagement((Application)app, mgmt); + return (T) app; + } else { + // currently, all brooklyn plans should produce the above; currently this will always throw Unsupported + try { + assembly = instantiator.instantiate(at, camp); + return (T) mgmt.getEntityManager().getEntity(assembly.getId()); + } catch (UnsupportedOperationException e) { + if (at.getPlatformComponentTemplates()==null || at.getPlatformComponentTemplates().isEmpty()) { + if (at.getCustomAttributes().containsKey("brooklyn.catalog")) + throw new IllegalArgumentException("Unrecognized application blueprint format: expected an application, not a brooklyn.catalog"); + throw new IllegalArgumentException("Unrecognized application blueprint format: no services defined"); + } + // map this (expected) error to a nicer message + throw new IllegalArgumentException("Unrecognized application blueprint format"); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + throw new IllegalArgumentException("Invalid plan: "+at, e); + } + } + } + + /** container for operation which creates something and which wants to return both + * the items created and any pending create/start task */ + public static class CreationResult<T,U> { + private final T thing; + @Nullable private final Task<U> task; + public CreationResult(T thing, Task<U> task) { + super(); + this.thing = thing; + this.task = task; + } + + protected static <T,U> CreationResult<T,U> of(T thing, @Nullable Task<U> task) { + return new CreationResult<T,U>(thing, task); + } + + /** returns the thing/things created */ + @Nullable public T get() { return thing; } + /** associated task, ie the one doing the creation/starting */ + public Task<U> task() { return task; } + public CreationResult<T,U> blockUntilComplete(Duration timeout) { if (task!=null) task.blockUntilEnded(timeout); return this; } + public CreationResult<T,U> blockUntilComplete() { if (task!=null) task.blockUntilEnded(); return this; } + } + + public static <T extends Application> CreationResult<T,Void> createStarting(ManagementContext mgmt, EntitySpec<T> appSpec) { + return start(createUnstarted(mgmt, appSpec)); + } + + public static CreationResult<? extends Application,Void> createStarting(ManagementContext mgmt, String appSpec) { + return start(createUnstarted(mgmt, appSpec)); + } + + public static CreationResult<? extends Application,Void> createStarting(ManagementContext mgmt, AssemblyTemplate at) { + return start(createUnstarted(mgmt, at)); + } + + public static <T extends Application> CreationResult<T,Void> start(T app) { + Task<Void> task = Entities.invokeEffector((EntityLocal)app, app, Startable.START, + // locations already set in the entities themselves; + // TODO make it so that this arg does not have to be supplied to START ! + MutableMap.of("locations", MutableList.of())); + return CreationResult.of(app, task); + } + + public static CreationResult<List<Entity>, List<String>> addChildren(final EntityLocal parent, String yaml, Boolean start) { + if (Boolean.FALSE.equals(start)) + return CreationResult.of(addChildrenUnstarted(parent, yaml), null); + return addChildrenStarting(parent, yaml); + } + + /** adds entities from the given yaml, under the given parent; but does not start them */ + public static List<Entity> addChildrenUnstarted(final EntityLocal parent, String yaml) { + log.debug("Creating child of "+parent+" from yaml:\n{}", yaml); + + ManagementContext mgmt = parent.getApplication().getManagementContext(); + CampPlatform camp = BrooklynServerConfig.getCampPlatform(mgmt).get(); + + AssemblyTemplate at = camp.pdp().registerDeploymentPlan( new StringReader(yaml) ); + + AssemblyTemplateInstantiator instantiator; + try { + instantiator = at.getInstantiator().newInstance(); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + if (instantiator instanceof AssemblyTemplateSpecInstantiator) { + BrooklynClassLoadingContext loader = JavaBrooklynClassLoadingContext.create(mgmt); + EntitySpec<?> specA = ((AssemblyTemplateSpecInstantiator) instantiator).createSpec(at, camp, loader, false); + + // see whether we can promote children + List<EntitySpec<?>> specs = MutableList.of(); + if (hasNoNameOrCustomKeysOrRoot(at, specA)) { + // we can promote + for (EntitySpec<?> specC: specA.getChildren()) { + collapseSpec(specA, specC); + specs.add(specC); + } + } else { + // if not promoting, set a nice name if needed + if (Strings.isEmpty(specA.getDisplayName())) { + int size = specA.getChildren().size(); + String childrenCountString = size+" "+(size!=1 ? "children" : "child"); + specA.displayName("Dynamically added "+childrenCountString); + } + specs.add(specA); + } + + final List<Entity> children = MutableList.of(); + for (EntitySpec<?> spec: specs) { + Entity child = (Entity)parent.addChild(spec); + Entities.manage(child); + children.add(child); + } + + return children; + } else { + throw new IllegalStateException("Spec could not be parsed to supply a compatible instantiator"); + } + } + + public static CreationResult<List<Entity>,List<String>> addChildrenStarting(final EntityLocal parent, String yaml) { + final List<Entity> children = addChildrenUnstarted(parent, yaml); + String childrenCountString; + + int size = children.size(); + childrenCountString = size+" "+(size!=1 ? "children" : "child"); + + TaskBuilder<List<String>> taskM = Tasks.<List<String>>builder().name("add children") + .dynamic(true) + .tag(BrooklynTaskTags.NON_TRANSIENT_TASK_TAG) + .body(new Callable<List<String>>() { + @Override public List<String> call() throws Exception { + return ImmutableList.copyOf(Iterables.transform(children, EntityFunctions.id())); + }}) + .description("Add and start "+childrenCountString); + + TaskBuilder<?> taskS = Tasks.builder().parallel(true).name("add (parallel)").description("Start each new entity"); + + // autostart if requested + for (Entity child: children) { + if (child instanceof Startable) { + taskS.add(Effectors.invocation(child, Startable.START, ImmutableMap.of("locations", ImmutableList.of()))); + } else { + // include a task, just to give feedback in the GUI + taskS.add(Tasks.builder().name("create").description("Skipping start (not a Startable Entity)") + .body(new Runnable() { public void run() {} }) + .tag(BrooklynTaskTags.tagForTargetEntity(child)) + .build()); + } + } + taskM.add(taskS.build()); + Task<List<String>> task = Entities.submit(parent, taskM.build()); + + return CreationResult.of(children, task); + } + + /** worker method to combine specs */ + @Beta //where should this live long-term? + public static void collapseSpec(EntitySpec<?> sourceToBeCollapsed, EntitySpec<?> targetToBeExpanded) { + if (Strings.isEmpty(targetToBeExpanded.getDisplayName())) + targetToBeExpanded.displayName(sourceToBeCollapsed.getDisplayName()); + if (!sourceToBeCollapsed.getLocations().isEmpty()) + targetToBeExpanded.locations(sourceToBeCollapsed.getLocations()); + + // NB: this clobbers child config; might prefer to deeply merge maps etc + // (but this should not be surprising, as unwrapping is often parameterising the nested blueprint, so outer config should dominate) + targetToBeExpanded.configure(sourceToBeCollapsed.getConfig()); + targetToBeExpanded.configure(sourceToBeCollapsed.getFlags()); + + // TODO copying tags to all entities is not ideal; + // in particular the BrooklynTags.YAML_SPEC tag will show all entities if the root has multiple + targetToBeExpanded.tags(sourceToBeCollapsed.getTags()); + } + + /** worker method to help determine whether child/children can be promoted */ + @Beta //where should this live long-term? + public static boolean hasNoNameOrCustomKeysOrRoot(AssemblyTemplate template, EntitySpec<?> spec) { + if (Strings.isNonEmpty(template.getName())) { + if (spec.getChildren().size()==1) { + String childName = Iterables.getOnlyElement(spec.getChildren()).getDisplayName(); + if (Strings.isEmpty(childName) || childName.equals(template.getName())) { + // if child has no name, or it's the same, could still promote + } else { + return false; + } + } else { + // if name set at root and promoting children would be ambiguous, do not promote + return false; + } + } else if (spec.getChildren().size()>1) { + // don't allow multiple children if a name is specified as a root + return false; + } + + Set<String> rootAttrs = template.getCustomAttributes().keySet(); + for (String rootAttr: rootAttrs) { + if (rootAttr.equals("brooklyn.catalog") || rootAttr.equals("brooklyn.config")) { + // these do not block promotion + continue; + } + if (rootAttr.startsWith("brooklyn.")) { + // any others in 'brooklyn' namespace will block promotion + return false; + } + // location is allowed in both, and is copied on promotion + // (name also copied) + // others are root currently are ignored on promotion; they are usually metadata + // TODO might be nice to know what we are excluding + } + + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagerInternal.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagerInternal.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagerInternal.java new file mode 100644 index 0000000..08f4069 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/EntityManagerInternal.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +import org.apache.brooklyn.api.entity.Application; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.management.EntityManager; + +public interface EntityManagerInternal extends EntityManager, BrooklynObjectManagerInternal<Entity> { + + /** gets all entities currently known to the application, including entities that are not yet managed */ + Iterable<Entity> getAllEntitiesInApplication(Application application); + + public Iterable<String> getEntityIds(); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/GroovyObservablesPropertyChangeToCollectionChangeAdapter.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/GroovyObservablesPropertyChangeToCollectionChangeAdapter.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/GroovyObservablesPropertyChangeToCollectionChangeAdapter.java new file mode 100644 index 0000000..4e74904 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/GroovyObservablesPropertyChangeToCollectionChangeAdapter.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +import groovy.util.ObservableList; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class GroovyObservablesPropertyChangeToCollectionChangeAdapter implements PropertyChangeListener { + @SuppressWarnings("rawtypes") + private final CollectionChangeListener delegate; + + public GroovyObservablesPropertyChangeToCollectionChangeAdapter(@SuppressWarnings("rawtypes") CollectionChangeListener delegate) { + this.delegate = delegate; + } + + @SuppressWarnings("unchecked") + public void propertyChange(PropertyChangeEvent evt) { + if (evt instanceof ObservableList.ElementAddedEvent) { + delegate.onItemAdded(evt.getNewValue()); + } else if (evt instanceof ObservableList.ElementRemovedEvent) { + delegate.onItemRemoved(evt.getOldValue()); + } else if (evt instanceof ObservableList.ElementUpdatedEvent) { + delegate.onItemRemoved(evt.getOldValue()); + delegate.onItemAdded(evt.getNewValue()); + } else if (evt instanceof ObservableList.ElementClearedEvent) { + for (Object value : ((ObservableList.ElementClearedEvent) evt).getValues()) { + delegate.onItemAdded(value); + } + } else if(evt instanceof ObservableList.MultiElementAddedEvent ) { + for(Object value: ((ObservableList.MultiElementAddedEvent)evt).getValues()){ + delegate.onItemAdded(value); + } + } + } + + public int hashCode() { + return delegate.hashCode(); + } + + public boolean equals(Object other) { + if (other instanceof GroovyObservablesPropertyChangeToCollectionChangeAdapter) + return delegate.equals(((GroovyObservablesPropertyChangeToCollectionChangeAdapter) other).delegate); + if (other instanceof CollectionChangeListener) + return delegate.equals(other); + return false; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6602f694/core/src/main/java/org/apache/brooklyn/core/management/internal/LocalAccessManager.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/management/internal/LocalAccessManager.java b/core/src/main/java/org/apache/brooklyn/core/management/internal/LocalAccessManager.java new file mode 100644 index 0000000..2699676 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/management/internal/LocalAccessManager.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.brooklyn.core.management.internal; + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.management.AccessController; + +import com.google.common.annotations.Beta; + +@Beta +public class LocalAccessManager implements AccessManager { + + private volatile boolean locationProvisioningAllowed = true; + private volatile boolean locationManagementAllowed = true; + private volatile boolean entityManagementAllowed = true; + + private final AtomicReference<AccessControllerImpl> controller = new AtomicReference<AccessControllerImpl>(); + + public LocalAccessManager() { + updateAccessController(); + } + + @Override + public AccessController getAccessController() { + return controller.get(); + } + + @Override + public boolean isLocationProvisioningAllowed() { + return locationProvisioningAllowed; + } + + @Override + public boolean isLocationManagementAllowed() { + return locationManagementAllowed; + } + + @Override + public boolean isEntityManagementAllowed() { + return entityManagementAllowed; + } + + @Override + public void setLocationProvisioningAllowed(boolean allowed) { + locationProvisioningAllowed = allowed; + updateAccessController(); + } + + @Override + public void setLocationManagementAllowed(boolean allowed) { + locationManagementAllowed = allowed; + updateAccessController(); + } + + @Override + public void setEntityManagementAllowed(boolean allowed) { + entityManagementAllowed = allowed; + updateAccessController(); + } + + private void updateAccessController() { + controller.set(new AccessControllerImpl(locationProvisioningAllowed, locationManagementAllowed, entityManagementAllowed)); + } + + private static class AccessControllerImpl implements AccessController { + private final boolean locationProvisioningAllowed; + private final boolean locationManagementAllowed; + private final boolean entityManagementAllowed; + + public AccessControllerImpl(boolean locationProvisioningAllowed, boolean locationManagementAllowed, + boolean entityManagementAllowed) { + this.locationProvisioningAllowed = locationProvisioningAllowed; + this.locationManagementAllowed = locationManagementAllowed; + this.entityManagementAllowed = entityManagementAllowed; + } + + @Override + public Response canProvisionLocation(Location provisioner) { + return (locationProvisioningAllowed ? Response.allowed() : Response.disallowed("location provisioning disabled")); + } + + @Override + public Response canManageLocation(Location loc) { + return (locationManagementAllowed ? Response.allowed() : Response.disallowed("location management disabled")); + } + + @Override + public Response canManageEntity(Entity entity) { + return (entityManagementAllowed ? Response.allowed() : Response.disallowed("entity management disabled")); + } + } +}
