http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/AbstractLocation.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/AbstractLocation.java b/core/src/main/java/org/apache/brooklyn/location/basic/AbstractLocation.java new file mode 100644 index 0000000..eee375b --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/AbstractLocation.java @@ -0,0 +1,708 @@ +/* + * 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.location.basic; + +import static brooklyn.util.GroovyJavaMethods.elvis; +import static brooklyn.util.JavaGroovyEquivalents.groovyTruth; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.Closeable; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.brooklyn.api.entity.rebind.RebindSupport; +import org.apache.brooklyn.api.entity.trait.Configurable; +import org.apache.brooklyn.api.management.Task; +import org.apache.brooklyn.location.Location; +import org.apache.brooklyn.mementos.LocationMemento; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.basic.AbstractBrooklynObject; +import brooklyn.config.ConfigInheritance; +import brooklyn.config.ConfigKey; +import brooklyn.config.ConfigKey.HasConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.entity.rebind.BasicLocationRebindSupport; +import brooklyn.event.basic.BasicConfigKey; +import brooklyn.internal.BrooklynFeatureEnablement; +import brooklyn.internal.storage.BrooklynStorage; +import brooklyn.internal.storage.Reference; +import brooklyn.internal.storage.impl.BasicReference; +import org.apache.brooklyn.location.LocationSpec; +import org.apache.brooklyn.location.geo.HasHostGeoInfo; +import org.apache.brooklyn.location.geo.HostGeoInfo; +import brooklyn.management.internal.LocalLocationManager; +import brooklyn.management.internal.ManagementContextInternal; +import brooklyn.util.collections.SetFromLiveMap; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.flags.FlagUtils; +import brooklyn.util.flags.TypeCoercions; +import brooklyn.util.guava.Maybe; +import brooklyn.util.stream.Streams; + +import com.google.common.base.Objects; +import com.google.common.base.Objects.ToStringHelper; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.reflect.TypeToken; + +/** + * A basic implementation of the {@link Location} interface. + * + * This provides an implementation which works according to the requirements of + * the interface documentation, and is ready to be extended to make more specialized locations. + * + * Override {@link #configure(Map)} to add special initialization logic. + */ +public abstract class AbstractLocation extends AbstractBrooklynObject implements LocationInternal, HasHostGeoInfo, Configurable { + + private static final long serialVersionUID = -7495805474138619830L; + + /** @deprecated since 0.7.0 shouldn't be public */ + @Deprecated + public static final Logger LOG = LoggerFactory.getLogger(AbstractLocation.class); + + public static final ConfigKey<Location> PARENT_LOCATION = new BasicConfigKey<Location>(Location.class, "parentLocation"); + + public static final ConfigKey<Boolean> TEMPORARY_LOCATION = ConfigKeys.newBooleanConfigKey("temporaryLocation", + "Indicates that the location is a temporary location that has been created to test connectivity, and that" + + "the location's events should not be recorded by usage listeners", false); + + private final AtomicBoolean configured = new AtomicBoolean(); + + private Reference<Long> creationTimeUtc = new BasicReference<Long>(System.currentTimeMillis()); + + // _not_ set from flag; configured explicitly in configure, because we also need to update the parent's list of children + private Reference<Location> parent = new BasicReference<Location>(); + + // NB: all accesses should be synchronized + private Set<Location> children = Sets.newLinkedHashSet(); + + private Reference<String> name = new BasicReference<String>(); + private boolean displayNameAutoGenerated = true; + + private Reference<HostGeoInfo> hostGeoInfo = new BasicReference<HostGeoInfo>(); + + private BasicConfigurationSupport config = new BasicConfigurationSupport(); + + private ConfigBag configBag = new ConfigBag(); + + private volatile boolean managed; + + private boolean inConstruction; + + private Reference<Map<Class<?>, Object>> extensions = new BasicReference<Map<Class<?>, Object>>(Maps.<Class<?>, Object>newConcurrentMap()); + + private final LocationDynamicType locationType; + + /** + * Construct a new instance of an AbstractLocation. + */ + public AbstractLocation() { + this(Maps.newLinkedHashMap()); + } + + /** + * Construct a new instance of an AbstractLocation. + * + * The properties map recognizes the following keys: + * <ul> + * <li>name - a name for the location + * <li>parentLocation - the parent {@link Location} + * </ul> + * + * Other common properties (retrieved via get/findLocationProperty) include: + * <ul> + * <li>latitude + * <li>longitude + * <li>displayName + * <li>iso3166 - list of iso3166-2 code strings + * <li>timeZone + * <li>abbreviatedName + * </ul> + */ + public AbstractLocation(Map<?,?> properties) { + super(properties); + inConstruction = true; + + // When one calls getConfig(key), we want to use the default value specified on *this* location + // if it overrides the default config, by using the type object + locationType = new LocationDynamicType(this); + + if (isLegacyConstruction()) { + AbstractBrooklynObject checkWeGetThis = configure(properties); + assert this.equals(checkWeGetThis) : this+" configure method does not return itself; returns "+checkWeGetThis+" instead of "+this; + + boolean deferConstructionChecks = (properties.containsKey("deferConstructionChecks") && TypeCoercions.coerce(properties.get("deferConstructionChecks"), Boolean.class)); + if (!deferConstructionChecks) { + FlagUtils.checkRequiredFields(this); + } + } + + inConstruction = false; + } + + protected void assertNotYetManaged() { + if (!inConstruction && Locations.isManaged(this)) { + LOG.warn("Configuration being made to {} after deployment; may not be supported in future versions", this); + } + //throw new IllegalStateException("Cannot set configuration "+key+" on active location "+this) + } + + public void setManagementContext(ManagementContextInternal managementContext) { + super.setManagementContext(managementContext); + if (displayNameAutoGenerated && getId() != null) name.set(getClass().getSimpleName()+":"+getId().substring(0, Math.min(getId().length(),4))); + + if (BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_USE_BROOKLYN_LIVE_OBJECTS_DATAGRID_STORAGE)) { + Location oldParent = parent.get(); + Set<Location> oldChildren = children; + Map<String, Object> oldConfig = configBag.getAllConfig(); + Long oldCreationTimeUtc = creationTimeUtc.get(); + String oldDisplayName = name.get(); + HostGeoInfo oldHostGeoInfo = hostGeoInfo.get(); + + parent = managementContext.getStorage().getReference(getId()+"-parent"); + children = SetFromLiveMap.create(managementContext.getStorage().<Location,Boolean>getMap(getId()+"-children")); + creationTimeUtc = managementContext.getStorage().getReference(getId()+"-creationTime"); + hostGeoInfo = managementContext.getStorage().getReference(getId()+"-hostGeoInfo"); + name = managementContext.getStorage().getReference(getId()+"-displayName"); + + // Only override stored defaults if we have actual values. We might be in setManagementContext + // because we are reconstituting an existing entity in a new brooklyn management-node (in which + // case believe what is already in the storage), or we might be in the middle of creating a new + // entity. Normally for a new entity (using EntitySpec creation approach), this will get called + // before setting the parent etc. However, for backwards compatibility we still support some + // things calling the entity's constructor directly. + if (oldParent != null) parent.set(oldParent); + if (oldChildren.size() > 0) children.addAll(oldChildren); + if (creationTimeUtc.isNull()) creationTimeUtc.set(oldCreationTimeUtc); + if (hostGeoInfo.isNull()) hostGeoInfo.set(oldHostGeoInfo); + if (name.isNull()) { + name.set(oldDisplayName); + } else { + displayNameAutoGenerated = false; + } + + configBag = ConfigBag.newLiveInstance(managementContext.getStorage().<String,Object>getMap(getId()+"-config")); + if (oldConfig.size() > 0) { + configBag.putAll(oldConfig); + } + } + } + + /** + * @deprecated since 0.7.0; only used for legacy brooklyn types where constructor is called directly; + * see overridden method for more info + */ + @SuppressWarnings("serial") + @Override + @Deprecated + public AbstractLocation configure(Map<?,?> properties) { + assertNotYetManaged(); + + boolean firstTime = !configured.getAndSet(true); + + configBag.putAll(properties); + + if (properties.containsKey(PARENT_LOCATION.getName())) { + // need to ensure parent's list of children is also updated + setParent(configBag.get(PARENT_LOCATION)); + + // don't include parentLocation in configBag, as breaks rebind + configBag.remove(PARENT_LOCATION); + } + + // NB: flag-setting done here must also be done in BasicLocationRebindSupport + FlagUtils.setFieldsFromFlagsWithBag(this, properties, configBag, firstTime); + FlagUtils.setAllConfigKeys(this, configBag, false); + + if (properties.containsKey("displayName")) { + name.set((String) removeIfPossible(properties, "displayName")); + displayNameAutoGenerated = false; + } else if (properties.containsKey("name")) { + name.set((String) removeIfPossible(properties, "name")); + displayNameAutoGenerated = false; + } else if (isLegacyConstruction()) { + name.set(getClass().getSimpleName()+":"+getId().substring(0, Math.min(getId().length(),4))); + displayNameAutoGenerated = true; + } + + // TODO Explicitly dealing with iso3166 here because want custom splitter rule comma-separated string. + // Is there a better way to do it (e.g. more similar to latitude, where configKey+TypeCoercion is enough)? + if (groovyTruth(properties.get("iso3166"))) { + Object rawCodes = removeIfPossible(properties, "iso3166"); + Set<String> codes; + if (rawCodes instanceof CharSequence) { + codes = ImmutableSet.copyOf(Splitter.on(",").trimResults().split((CharSequence)rawCodes)); + } else { + codes = TypeCoercions.coerce(rawCodes, new TypeToken<Set<String>>() {}); + } + configBag.put(LocationConfigKeys.ISO_3166, codes); + } + + return this; + } + + // TODO ensure no callers rely on 'remove' semantics, and don't remove; + // or perhaps better use a config bag so we know what is used v unused + private static Object removeIfPossible(Map<?,?> map, Object key) { + try { + return map.remove(key); + } catch (Exception e) { + return map.get(key); + } + } + + public boolean isManaged() { + return getManagementContext() != null && managed; + } + + public void onManagementStarted() { + if (displayNameAutoGenerated) name.set(getClass().getSimpleName()+":"+getId().substring(0, Math.min(getId().length(),4))); + this.managed = true; + } + + public void onManagementStopped() { + this.managed = false; + if (getManagementContext().isRunning()) { + BrooklynStorage storage = ((ManagementContextInternal)getManagementContext()).getStorage(); + storage.remove(getId()+"-parent"); + storage.remove(getId()+"-children"); + storage.remove(getId()+"-creationTime"); + storage.remove(getId()+"-hostGeoInfo"); + storage.remove(getId()+"-displayName"); + storage.remove(getId()+"-config"); + } + } + + @Override + public String getDisplayName() { + return name.get(); + } + + protected boolean isDisplayNameAutoGenerated() { + return displayNameAutoGenerated; + } + + @Override + public Location getParent() { + return parent.get(); + } + + @Override + public Collection<Location> getChildren() { + synchronized (children) { + return ImmutableList.copyOf(children); + } + } + + @Override + public void setParent(Location newParent) { + setParent(newParent, true); + } + + public void setParent(Location newParent, boolean updateChildListParents) { + if (newParent == this) { + throw new IllegalArgumentException("Location cannot be its own parent: "+this); + } + if (newParent == parent.get()) { + return; // no-op; already have desired parent + } + + if (parent.get() != null) { + Location oldParent = parent.get(); + parent.set(null); + if (updateChildListParents) + ((AbstractLocation)oldParent).removeChild(this); + } + // TODO Should we support a location changing parent? The resulting unmanage/manage might cause problems. + // The code above suggests we do, but maybe we should warn or throw error, or at least test it! + + parent.set(newParent); + if (newParent != null) { + if (updateChildListParents) + ((AbstractLocation)newParent).addChild(this); + } + + onChanged(); + } + + @Override + public ConfigurationSupportInternal config() { + return config ; + } + + private class BasicConfigurationSupport implements ConfigurationSupportInternal { + + @Override + public <T> T get(ConfigKey<T> key) { + if (hasConfig(key, false)) return getLocalBag().get(key); + if (getParent() != null && isInherited(key)) { + return getParent().getConfig(key); + } + + // In case this entity class has overridden the given key (e.g. to set default), then retrieve this entity's key + // TODO when locations become entities, the duplication of this compared to EntityConfigMap.getConfig will disappear. + @SuppressWarnings("unchecked") + ConfigKey<T> ownKey = (ConfigKey<T>) elvis(locationType.getConfigKey(key.getName()), key); + + return ownKey.getDefaultValue(); + } + + @Override + public <T> T get(HasConfigKey<T> key) { + return get(key.getConfigKey()); + } + + @Override + public <T> T set(ConfigKey<T> key, T val) { + T result = configBag.put(key, val); + onChanged(); + return result; + } + + @Override + public <T> T set(HasConfigKey<T> key, T val) { + return set(key.getConfigKey(), val); + } + + @Override + public <T> T set(ConfigKey<T> key, Task<T> val) { + // TODO Support for locations + throw new UnsupportedOperationException(); + } + + @Override + public <T> T set(HasConfigKey<T> key, Task<T> val) { + // TODO Support for locations + throw new UnsupportedOperationException(); + } + + @Override + public ConfigBag getBag() { + ConfigBag result = ConfigBag.newInstanceExtending(configBag, ImmutableMap.of()); + Location p = getParent(); + if (p!=null) result.putIfAbsent(((LocationInternal)p).config().getBag()); + return result; + } + + @Override + public ConfigBag getLocalBag() { + return configBag; + } + + @Override + public Maybe<Object> getRaw(ConfigKey<?> key) { + if (hasConfig(key, false)) return Maybe.of(getLocalBag().getStringKey(key.getName())); + if (getParent() != null && isInherited(key)) return ((LocationInternal)getParent()).config().getRaw(key); + return Maybe.absent(); + } + + @Override + public Maybe<Object> getRaw(HasConfigKey<?> key) { + return getRaw(key.getConfigKey()); + } + + @Override + public Maybe<Object> getLocalRaw(ConfigKey<?> key) { + if (hasConfig(key, false)) return Maybe.of(getLocalBag().getStringKey(key.getName())); + return Maybe.absent(); + } + + @Override + public Maybe<Object> getLocalRaw(HasConfigKey<?> key) { + return getLocalRaw(key.getConfigKey()); + } + + @Override + public void addToLocalBag(Map<String, ?> vals) { + configBag.putAll(vals); + } + + @Override + public void removeFromLocalBag(String key) { + configBag.remove(key); + } + + @Override + public void refreshInheritedConfig() { + // no-op for location + } + + @Override + public void refreshInheritedConfigOfChildren() { + // no-op for location + } + + private boolean hasConfig(ConfigKey<?> key, boolean includeInherited) { + if (includeInherited && isInherited(key)) { + return getBag().containsKey(key); + } else { + return getLocalBag().containsKey(key); + } + } + + private boolean isInherited(ConfigKey<?> key) { + ConfigInheritance inheritance = key.getInheritance(); + if (inheritance==null) inheritance = getDefaultInheritance(); + return inheritance.isInherited(key, getParent(), AbstractLocation.this); + } + + private ConfigInheritance getDefaultInheritance() { + return ConfigInheritance.ALWAYS; + } + } + + @Override + public <T> T getConfig(HasConfigKey<T> key) { + return config().get(key); + } + + @Override + public <T> T getConfig(ConfigKey<T> key) { + return config().get(key); + } + + @Override + @Deprecated + public boolean hasConfig(ConfigKey<?> key, boolean includeInherited) { + return config.hasConfig(key, includeInherited); + } + + @Override + @Deprecated + public Map<String,Object> getAllConfig(boolean includeInherited) { + // TODO Have no information about what to include/exclude inheritance wise. + // however few things use getAllConfigBag() + ConfigBag bag = (includeInherited ? config().getBag() : config().getLocalBag()); + return bag.getAllConfig(); + } + + @Override + @Deprecated + public ConfigBag getAllConfigBag() { + // TODO see comments in EntityConfigMap and on interface methods. + // here ConfigBag is used exclusively so + // we have no information about what to include/exclude inheritance wise. + // however few things use getAllConfigBag() + return config().getBag(); + } + + @Override + public ConfigBag getLocalConfigBag() { + return config().getLocalBag(); + } + + /** + * @deprecated since 0.7; use {@link #getLocalConfigBag()} + * @since 0.6 + */ + @Deprecated + public ConfigBag getRawLocalConfigBag() { + return config().getLocalBag(); + } + + @Override + @Deprecated + public <T> T setConfig(ConfigKey<T> key, T value) { + return config().set(key, value); + } + + /** + * @since 0.6.0 (?) - use getDisplayName + * @deprecated since 0.7.0; use {@link #getDisplayName()} + */ + @Deprecated + public void setName(String newName) { + setDisplayName(newName); + } + + public void setDisplayName(String newName) { + name.set(newName); + displayNameAutoGenerated = false; + onChanged(); + } + + @Override + public boolean equals(Object o) { + if (! (o instanceof Location)) { + return false; + } + + Location l = (Location) o; + return getId().equals(l.getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + @Override + public boolean containsLocation(Location potentialDescendent) { + Location loc = potentialDescendent; + while (loc != null) { + if (this == loc) return true; + loc = loc.getParent(); + } + return false; + } + + protected <T extends Location> T addChild(LocationSpec<T> spec) { + T child = getManagementContext().getLocationManager().createLocation(spec); + addChild(child); + return child; + } + + @SuppressWarnings("deprecation") + public void addChild(Location child) { + // Previously, setParent delegated to addChildLocation and we sometimes ended up with + // duplicate entries here. Instead this now uses a similar scheme to + // AbstractLocation.setParent/addChild (with any weaknesses for distribution that such a + // scheme might have...). + // + // We continue to use a list to allow identical-looking locations, but they must be different + // instances. + + synchronized (children) { + for (Location contender : children) { + if (contender == child) { + // don't re-add; no-op + return; + } + } + + children.add(child); + } + + if (isManaged()) { + if (!getManagementContext().getLocationManager().isManaged(child)) { + Locations.manage(child, getManagementContext()); + } + } else if (getManagementContext() != null) { + if (((LocalLocationManager)getManagementContext().getLocationManager()).getLocationEvenIfPreManaged(child.getId()) == null) { + ((ManagementContextInternal)getManagementContext()).prePreManage(child); + } + } + + children.add(child); + child.setParent(this); + + onChanged(); + } + + public boolean removeChild(Location child) { + boolean removed; + synchronized (children) { + removed = children.remove(child); + } + if (removed) { + if (child instanceof Closeable) { + Streams.closeQuietly((Closeable)child); + } + child.setParent(null); + + if (isManaged()) { + getManagementContext().getLocationManager().unmanage(child); + } + } + onChanged(); + return removed; + } + + protected void onChanged() { + // currently changes simply trigger re-persistence; there is no intermediate listener as we do for EntityChangeListener + if (isManaged()) { + getManagementContext().getRebindManager().getChangeListener().onChanged(this); + } + } + + /** Default String representation is simplified name of class, together with selected fields. */ + @Override + public String toString() { + return string().toString(); + } + + @Override + public String toVerboseString() { + return toString(); + } + + /** override this, adding to the returned value, to supply additional fields to include in the toString */ + protected ToStringHelper string() { + return Objects.toStringHelper(getClass()).add("id", getId()).add("name", name); + } + + @Override + public HostGeoInfo getHostGeoInfo() { return hostGeoInfo.get(); } + + public void setHostGeoInfo(HostGeoInfo hostGeoInfo) { + if (hostGeoInfo!=null) { + this.hostGeoInfo.set(hostGeoInfo); + setConfig(LocationConfigKeys.LATITUDE, hostGeoInfo.latitude); + setConfig(LocationConfigKeys.LONGITUDE, hostGeoInfo.longitude); + } + } + + @Override + public RebindSupport<LocationMemento> getRebindSupport() { + return new BasicLocationRebindSupport(this); + } + + @Override + public boolean hasExtension(Class<?> extensionType) { + return extensions.get().containsKey(checkNotNull(extensionType, "extensionType")); + } + + @Override + @SuppressWarnings("unchecked") + public <T> T getExtension(Class<T> extensionType) { + Object extension = extensions.get().get(checkNotNull(extensionType, "extensionType")); + if (extension == null) { + throw new IllegalArgumentException("No extension of type "+extensionType+" registered for location "+this); + } + return (T) extension; + } + + @Override + public <T> void addExtension(Class<T> extensionType, T extension) { + checkNotNull(extensionType, "extensionType"); + checkNotNull(extension, "extension"); + checkArgument(extensionType.isInstance(extension), "extension %s does not implement %s", extension, extensionType); + extensions.get().put(extensionType, extension); + } + + @Override + public Map<String, String> toMetadataRecord() { + ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); + if (getDisplayName() != null) builder.put("displayName", getDisplayName()); + if (getParent() != null && getParent().getDisplayName() != null) { + builder.put("parentDisplayName", getParent().getDisplayName()); + } + return builder.build(); + } +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/AbstractLocationResolver.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/AbstractLocationResolver.java b/core/src/main/java/org/apache/brooklyn/location/basic/AbstractLocationResolver.java new file mode 100644 index 0000000..59a0771 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/AbstractLocationResolver.java @@ -0,0 +1,189 @@ +/* + * 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.location.basic; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.brooklyn.api.management.ManagementContext; +import org.apache.brooklyn.location.Location; +import org.apache.brooklyn.location.LocationRegistry; +import org.apache.brooklyn.location.LocationResolver; +import org.apache.brooklyn.location.LocationSpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.brooklyn.location.basic.AbstractLocationResolver.SpecParser.ParsedSpec; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.text.KeyValueParser; + +import com.google.common.collect.ImmutableList; + +/** + * Examples of valid specs: + * <ul> + * <li>byon(hosts=myhost) + * <li>byon(hosts=myhost,myhost2) + * <li>byon(hosts="myhost, myhost2") + * <li>byon(hosts=myhost,myhost2, name=abc) + * <li>byon(hosts="myhost, myhost2", name="my location name") + * </ul> + * + * @author aled + */ +@SuppressWarnings({"unchecked","rawtypes"}) +public abstract class AbstractLocationResolver implements LocationResolver { + + public static final Logger log = LoggerFactory.getLogger(AbstractLocationResolver.class); + + protected volatile ManagementContext managementContext; + + protected volatile SpecParser specParser; + + protected abstract Class<? extends Location> getLocationType(); + + protected abstract SpecParser getSpecParser(); + + @Override + public void init(ManagementContext managementContext) { + this.managementContext = checkNotNull(managementContext, "managementContext"); + this.specParser = getSpecParser(); + } + + @Override + public boolean accepts(String spec, LocationRegistry registry) { + return BasicLocationRegistry.isResolverPrefixForSpec(this, spec, true); + } + + @Override + public Location newLocationFromString(Map locationFlags, String spec, LocationRegistry registry) { + ConfigBag config = extractConfig(locationFlags, spec, registry); + Map globalProperties = registry.getProperties(); + String namedLocation = (String) locationFlags.get(LocationInternal.NAMED_SPEC_NAME.getName()); + + if (registry != null) { + LocationPropertiesFromBrooklynProperties.setLocalTempDir(globalProperties, config); + } + + return managementContext.getLocationManager().createLocation(LocationSpec.create(getLocationType()) + .configure(config.getAllConfig()) + .configure(LocationConfigUtils.finalAndOriginalSpecs(spec, locationFlags, globalProperties, namedLocation))); + } + + protected ConfigBag extractConfig(Map<?,?> locationFlags, String spec, LocationRegistry registry) { + Map globalProperties = registry.getProperties(); + ParsedSpec parsedSpec = specParser.parse(spec); + String namedLocation = (String) locationFlags.get(LocationInternal.NAMED_SPEC_NAME.getName()); + + // prefer args map over location flags + Map<String, Object> filteredProperties = getFilteredLocationProperties(getPrefix(), namedLocation, globalProperties); + ConfigBag flags = ConfigBag.newInstance(parsedSpec.argsMap).putIfAbsent(locationFlags).putIfAbsent(filteredProperties); + + return flags; + } + + protected Map<String, Object> getFilteredLocationProperties(String provider, String namedLocation, Map<String, ?> globalProperties) { + return new LocationPropertiesFromBrooklynProperties().getLocationProperties(getPrefix(), namedLocation, globalProperties); + } + + /** + * Parses a spec, by default of the general form "prefix:parts1:part2(arg1=val1,arg2=val2)" + */ + protected static class SpecParser { + + protected static class ParsedSpec { + public final String spec; + public final List<String> partsList; + public final Map<String,String> argsMap; + + ParsedSpec(String spec, List<String> partsList, Map<String,String> argsMap) { + this.spec = spec; + this.partsList = ImmutableList.copyOf(partsList); + this.argsMap = Collections.unmodifiableMap(MutableMap.copyOf(argsMap)); + } + } + + protected final String prefix; + protected final Pattern pattern; + private String exampleUsage; + + public SpecParser(String prefix) { + this.prefix = prefix; + pattern = Pattern.compile("("+prefix+"|"+prefix.toLowerCase()+"|"+prefix.toUpperCase()+")" + "(:)?" + "(\\((.*)\\))?$"); + } + + public SpecParser(String prefix, Pattern pattern) { + this.prefix = prefix; + this.pattern = pattern; + } + + public SpecParser setExampleUsage(String exampleUsage) { + this.exampleUsage = exampleUsage; + return this; + } + + protected String getUsage(String spec) { + if (exampleUsage == null) { + return "Spec should be in the form "+pattern; + } else { + return "for example, "+exampleUsage; + } + } + + protected void checkParsedSpec(ParsedSpec parsedSpec) { + // If someone tries "byon:(),byon:()" as a single spec, we get weird key-values! + for (String key : parsedSpec.argsMap.keySet()) { + if (key.contains(":") || key.contains("{") || key.contains("}") || key.contains("(") || key.contains(")")) { + throw new IllegalArgumentException("Invalid byon spec: "+parsedSpec.spec+" (key="+key+")"); + } + } + String name = parsedSpec.argsMap.get("name"); + if (parsedSpec.argsMap.containsKey("name") && (name == null || name.isEmpty())) { + throw new IllegalArgumentException("Invalid location '"+parsedSpec.spec+"'; if name supplied then value must be non-empty"); + } + String displayName = parsedSpec.argsMap.get("displayName"); + if (parsedSpec.argsMap.containsKey("displayName") && (displayName == null || displayName.isEmpty())) { + throw new IllegalArgumentException("Invalid location '"+parsedSpec.spec+"'; if displayName supplied then value must be non-empty"); + } + } + + public ParsedSpec parse(String spec) { + Matcher matcher = pattern.matcher(spec); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid location '"+spec+"'; "+getUsage(spec)); + } + + String argsPart = matcher.group(3); + if (argsPart != null && argsPart.startsWith("(") && argsPart.endsWith(")")) { + // TODO Hacky; hosts("1.1.1.1") returns argsPart=("1.1.1.1") + argsPart = argsPart.substring(1, argsPart.length()-1); + } + Map<String, String> argsMap = KeyValueParser.parseMap(argsPart); + ParsedSpec result = new ParsedSpec(spec, ImmutableList.<String>of(), argsMap); + checkParsedSpec(result); + return result; + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/AggregatingMachineProvisioningLocation.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/AggregatingMachineProvisioningLocation.java b/core/src/main/java/org/apache/brooklyn/location/basic/AggregatingMachineProvisioningLocation.java new file mode 100644 index 0000000..33e8efa --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/AggregatingMachineProvisioningLocation.java @@ -0,0 +1,139 @@ +/* + * 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.location.basic; + +import static com.google.common.base.Preconditions.checkState; + +import java.io.Closeable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.brooklyn.location.MachineLocation; +import org.apache.brooklyn.location.MachineProvisioningLocation; +import org.apache.brooklyn.location.NoMachinesAvailableException; +import brooklyn.util.flags.SetFromFlag; +import brooklyn.util.stream.Streams; + +import com.google.common.base.Objects; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +/** + * Takes a list of other provisioners, and round-robins across them when obtaining a machine. + */ +public class AggregatingMachineProvisioningLocation<T extends MachineLocation> extends AbstractLocation + implements MachineProvisioningLocation<T>, Closeable { + + private Object lock; + + @SetFromFlag + protected List<MachineProvisioningLocation<T>> provisioners; + + @SetFromFlag + protected Map<T, MachineProvisioningLocation<T>> inUse; + + protected final AtomicInteger obtainCounter = new AtomicInteger(); + + public AggregatingMachineProvisioningLocation() { + this(Maps.newLinkedHashMap()); + } + + public AggregatingMachineProvisioningLocation(Map properties) { + super(properties); + + if (isLegacyConstruction()) { + init(); + } + } + + @Override + public void init() { + super.init(); + } + + @Override + public String toVerboseString() { + return Objects.toStringHelper(this).omitNullValues() + .add("id", getId()).add("name", getDisplayName()) + .add("provisioners", provisioners) + .toString(); + } + + @Override + public AbstractLocation configure(Map<?,?> properties) { + if (lock == null) { + lock = new Object(); + provisioners = Lists.<MachineProvisioningLocation<T>>newArrayList(); + inUse = Maps.<T, MachineProvisioningLocation<T>>newLinkedHashMap(); + } + return super.configure(properties); + } + + @Override + public AggregatingMachineProvisioningLocation<T> newSubLocation(Map<?,?> newFlags) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + for (MachineProvisioningLocation<?> provisioner : provisioners) { + if (provisioner instanceof Closeable) { + Streams.closeQuietly((Closeable)provisioner); + } + } + } + + public T obtain() throws NoMachinesAvailableException { + return obtain(Maps.<String,Object>newLinkedHashMap()); + } + + @Override + public T obtain(Map<?,?> flags) throws NoMachinesAvailableException { + checkState(provisioners.size() > 0, "no provisioners!"); + int index = obtainCounter.getAndIncrement(); + for (int i = 0; i < provisioners.size(); i++) { + MachineProvisioningLocation<T> provisioner = provisioners.get(index++ % provisioners.size()); + try { + T machine = provisioner.obtain(flags); + inUse.put(machine, provisioner); + return machine; + } catch (NoMachinesAvailableException e) { + // move on; try next + } + } + throw new NoMachinesAvailableException("No machines available in "+toString()); + } + + @Override + public void release(T machine) { + MachineProvisioningLocation<T> provisioner = inUse.remove(machine); + if (provisioner != null) { + provisioner.release(machine); + } else { + throw new IllegalStateException("Request to release machine "+machine+", but this machine is not currently allocated"); + } + } + + @Override + public Map<String,Object> getProvisioningFlags(Collection<String> tags) { + return Maps.<String,Object>newLinkedHashMap(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/BasicHardwareDetails.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/BasicHardwareDetails.java b/core/src/main/java/org/apache/brooklyn/location/basic/BasicHardwareDetails.java new file mode 100644 index 0000000..fc90a68 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/BasicHardwareDetails.java @@ -0,0 +1,56 @@ +/* + * 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.location.basic; + +import javax.annotation.concurrent.Immutable; + +import com.google.common.base.Objects; + +import org.apache.brooklyn.location.HardwareDetails; + +@Immutable +public class BasicHardwareDetails implements HardwareDetails { + + private final Integer cpuCount; + private final Integer ram; + + public BasicHardwareDetails(Integer cpuCount, Integer ram) { + this.cpuCount = cpuCount; + this.ram = ram; + } + + @Override + public Integer getCpuCount() { + return cpuCount; + } + + @Override + public Integer getRam() { + return ram; + } + + @Override + public String toString() { + return Objects.toStringHelper(HardwareDetails.class) + .omitNullValues() + .add("cpuCount", cpuCount) + .add("ram", ram) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/BasicLocationDefinition.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/BasicLocationDefinition.java b/core/src/main/java/org/apache/brooklyn/location/basic/BasicLocationDefinition.java new file mode 100644 index 0000000..550ca12 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/BasicLocationDefinition.java @@ -0,0 +1,85 @@ +/* + * 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.location.basic; + +import java.util.Map; + +import org.apache.brooklyn.location.LocationDefinition; +import brooklyn.util.text.Identifiers; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; + +public class BasicLocationDefinition implements LocationDefinition { + + private final String id; + private final String name; + private final String spec; + private final Map<String,Object> config; + + public BasicLocationDefinition(String name, String spec, Map<String,? extends Object> config) { + this(Identifiers.makeRandomId(8), name, spec, config); + } + + public BasicLocationDefinition(String id, String name, String spec, Map<String,? extends Object> config) { + this.id = Preconditions.checkNotNull(id); + this.name = name; + this.spec = Preconditions.checkNotNull(spec); + this.config = config==null ? ImmutableMap.<String, Object>of() : ImmutableMap.<String, Object>copyOf(config); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getSpec() { + return spec; + } + + @Override + public Map<String, Object> getConfig() { + return config; + } + + @Override + public boolean equals(Object o) { + if (this==o) return true; + if ((o instanceof LocationDefinition) && id.equals(((LocationDefinition)o).getId())) return true; + return false; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return "LocationDefinition{" + + "id='" + getId() + '\'' + + ", name='" + getName() + '\'' + + ", spec='" + getSpec() + '\'' + + ", config=" + getConfig() + + '}'; + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/BasicLocationRegistry.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/BasicLocationRegistry.java b/core/src/main/java/org/apache/brooklyn/location/basic/BasicLocationRegistry.java new file mode 100644 index 0000000..1c21e32 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/BasicLocationRegistry.java @@ -0,0 +1,480 @@ +/* + * 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.location.basic; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.ServiceLoader; +import java.util.Set; + +import org.apache.brooklyn.location.Location; +import org.apache.brooklyn.location.LocationDefinition; +import org.apache.brooklyn.location.LocationRegistry; +import org.apache.brooklyn.location.LocationResolver; +import org.apache.brooklyn.location.LocationSpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.api.catalog.BrooklynCatalog; +import org.apache.brooklyn.api.catalog.CatalogItem; +import org.apache.brooklyn.api.management.ManagementContext; + +import brooklyn.catalog.CatalogPredicates; +import brooklyn.config.ConfigMap; +import brooklyn.config.ConfigPredicates; +import brooklyn.config.ConfigUtils; +import brooklyn.management.internal.LocalLocationManager; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.guava.Maybe; +import brooklyn.util.guava.Maybe.Absent; +import brooklyn.util.javalang.JavaClassNames; +import brooklyn.util.text.Identifiers; +import brooklyn.util.text.StringEscapes.JavaStringEscapes; +import brooklyn.util.text.WildcardGlobs; +import brooklyn.util.text.WildcardGlobs.PhraseTreatment; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; + +/** + * See {@link LocationRegistry} for general description. + * <p> + * TODO The relationship between the catalog and the location registry is a bit messy. + * For all existing code, the location registry is the definitive way to resolve + * locations. + * <p> + * Any location item added to the catalog must therefore be registered here. + * Whenever an item is added to the catalog, it will automatically call + * {@link #updateDefinedLocation(CatalogItem)}. Similarly, when a location + * is deleted from the catalog it will call {@link #removeDefinedLocation(CatalogItem)}. + * <p> + * However, the location item in the catalog has an unparsed blob of YAML, which contains + * important things like the type and the config of the location. This is only parsed when + * {@link BrooklynCatalog#createSpec(CatalogItem)} is called. We therefore jump through + * some hoops to wire together the catalog and the registry. + * <p> + * To add a location to the catalog, and then to resolve a location that is in the catalog, + * it goes through the following steps: + * + * <ol> + * <li>Call {@link BrooklynCatalog#addItems(String)} + * <ol> + * <li>This automatically calls {@link #updateDefinedLocation(CatalogItem)} + * <li>A LocationDefinition is creating, using as its id the {@link CatalogItem#getSymbolicName()}. + * The definition's spec is {@code brooklyn.catalog:<symbolicName>:<version>}, + * </ol> + * <li>A blueprint can reference the catalog item using its symbolic name, + * such as the YAML {@code location: my-new-location}. + * (this feels similar to the "named locations"). + * <ol> + * <li>This automatically calls {@link #resolve(String)}. + * <li>The LocationDefinition is found by lookig up this name. + * <li>The {@link LocationDefiniton.getSpec()} is retrieved; the right {@link LocationResolver} is + * found for it. + * <li>This uses the {@link CatalogLocationResolver}, because the spec starts with {@code brooklyn.catalog:}. + * <li>This resolver extracts from the spec the <symobolicName>:<version>, and looks up the + * catalog item using {@link BrooklynCatalog#getCatalogItem(String, String)}. + * <li>It then creates a {@link LocationSpec} by calling {@link BrooklynCatalog#createSpec(CatalogItem)}. + * <ol> + * <li>This first tries to use the type (that is in the YAML) as a simple Java class. + * <li>If that fails, it will resolve the type using {@link #resolve(String, Boolean, Map)}, which + * returns an actual location object. + * <li>It extracts from that location object the appropriate metadata to create a {@link LocationSpec}, + * returns the spec and discards the location object. + * </ol> + * <li>The resolver creates the {@link Location} from the {@link LocationSpec} + * </ol> + * </ol> + * + * There is no concept of a location version in this registry. The version + * in the catalog is generally ignored. + */ +@SuppressWarnings({"rawtypes","unchecked"}) +public class BasicLocationRegistry implements LocationRegistry { + + // TODO save / serialize + // (we persist live locations, ie those in the LocationManager, but not "catalog" locations, ie those in this Registry) + + public static final Logger log = LoggerFactory.getLogger(BasicLocationRegistry.class); + + /** + * Splits a comma-separated list of locations (names or specs) into an explicit list. + * The splitting is very careful to handle commas embedded within specs, to split correctly. + */ + public static List<String> expandCommaSeparateLocations(String locations) { + return WildcardGlobs.getGlobsAfterBraceExpansion("{"+locations+"}", false, PhraseTreatment.INTERIOR_NOT_EXPANDABLE, PhraseTreatment.INTERIOR_NOT_EXPANDABLE); + // don't do this, it tries to expand commas inside parentheses which is not good! +// QuotedStringTokenizer.builder().addDelimiterChars(",").buildList((String)id); + } + + private final ManagementContext mgmt; + /** map of defined locations by their ID */ + private final Map<String,LocationDefinition> definedLocations = new LinkedHashMap<String, LocationDefinition>(); + + protected final Map<String,LocationResolver> resolvers = new LinkedHashMap<String, LocationResolver>(); + + private final Set<String> specsWarnedOnException = Sets.newConcurrentHashSet(); + + public BasicLocationRegistry(ManagementContext mgmt) { + this.mgmt = checkNotNull(mgmt, "mgmt"); + findServices(); + updateDefinedLocations(); + } + + protected void findServices() { + ServiceLoader<LocationResolver> loader = ServiceLoader.load(LocationResolver.class, mgmt.getCatalogClassLoader()); + for (LocationResolver r: loader) { + registerResolver(r); + } + if (log.isDebugEnabled()) log.debug("Location resolvers are: "+resolvers); + if (resolvers.isEmpty()) log.warn("No location resolvers detected: is src/main/resources correctly included?"); + } + + /** Registers the given resolver, invoking {@link LocationResolver#init(ManagementContext)} on the argument + * and returning true, unless the argument indicates false for {@link LocationResolver.EnableableLocationResolver#isEnabled()} */ + public boolean registerResolver(LocationResolver r) { + r.init(mgmt); + if (r instanceof LocationResolver.EnableableLocationResolver) { + if (!((LocationResolver.EnableableLocationResolver)r).isEnabled()) { + return false; + } + } + resolvers.put(r.getPrefix(), r); + return true; + } + + @Override + public Map<String,LocationDefinition> getDefinedLocations() { + synchronized (definedLocations) { + return ImmutableMap.<String,LocationDefinition>copyOf(definedLocations); + } + } + + @Override + public LocationDefinition getDefinedLocationById(String id) { + return definedLocations.get(id); + } + + @Override + public LocationDefinition getDefinedLocationByName(String name) { + synchronized (definedLocations) { + for (LocationDefinition l: definedLocations.values()) { + if (l.getName().equals(name)) return l; + } + return null; + } + } + + @Override + public void updateDefinedLocation(LocationDefinition l) { + synchronized (definedLocations) { + definedLocations.put(l.getId(), l); + } + } + + /** + * Converts the given item from the catalog into a LocationDefinition, and adds it + * to the registry (overwriting anything already registered with the id + * {@link CatalogItem#getCatalogItemId()}. + */ + public void updateDefinedLocation(CatalogItem<Location, LocationSpec<?>> item) { + String id = item.getCatalogItemId(); + String symbolicName = item.getSymbolicName(); + String spec = CatalogLocationResolver.NAME + ":" + id; + Map<String, Object> config = ImmutableMap.<String, Object>of(); + BasicLocationDefinition locDefinition = new BasicLocationDefinition(symbolicName, symbolicName, spec, config); + + updateDefinedLocation(locDefinition); + } + + public void removeDefinedLocation(CatalogItem<Location, LocationSpec<?>> item) { + removeDefinedLocation(item.getSymbolicName()); + } + + @Override + public void removeDefinedLocation(String id) { + LocationDefinition removed; + synchronized (definedLocations) { + removed = definedLocations.remove(id); + } + if (removed == null && log.isDebugEnabled()) { + log.debug("{} was asked to remove location with id {} but no such location was registered", this, id); + } + } + + public void updateDefinedLocations() { + synchronized (definedLocations) { + // first read all properties starting brooklyn.location.named.xxx + // (would be nice to move to a better way, e.g. yaml, then deprecate this approach, but first + // we need ability/format for persisting named locations, and better support for adding+saving via REST/GUI) + int count = 0; + String NAMED_LOCATION_PREFIX = "brooklyn.location.named."; + ConfigMap namedLocationProps = mgmt.getConfig().submap(ConfigPredicates.startingWith(NAMED_LOCATION_PREFIX)); + for (String k: namedLocationProps.asMapWithStringKeys().keySet()) { + String name = k.substring(NAMED_LOCATION_PREFIX.length()); + // If has a dot, then is a sub-property of a named location (e.g. brooklyn.location.named.prod1.user=bob) + if (!name.contains(".")) { + // this is a new named location + String spec = (String) namedLocationProps.asMapWithStringKeys().get(k); + // make up an ID + String id = Identifiers.makeRandomId(8); + Map<String, Object> config = ConfigUtils.filterForPrefixAndStrip(namedLocationProps.asMapWithStringKeys(), k+"."); + definedLocations.put(id, new BasicLocationDefinition(id, name, spec, config)); + count++; + } + } + if (log.isDebugEnabled()) + log.debug("Found "+count+" defined locations from properties (*.named.* syntax): "+definedLocations.values()); + if (getDefinedLocationByName("localhost")==null && !BasicOsDetails.Factory.newLocalhostInstance().isWindows() + && LocationConfigUtils.isEnabled(mgmt, "brooklyn.location.localhost")) { + log.debug("Adding a defined location for localhost"); + // add 'localhost' *first* + ImmutableMap<String, LocationDefinition> oldDefined = ImmutableMap.copyOf(definedLocations); + definedLocations.clear(); + String id = Identifiers.makeRandomId(8); + definedLocations.put(id, localhost(id)); + definedLocations.putAll(oldDefined); + } + + for (CatalogItem<Location, LocationSpec<?>> item : mgmt.getCatalog().getCatalogItems(CatalogPredicates.IS_LOCATION)) { + updateDefinedLocation(item); + count++; + } + } + } + + @VisibleForTesting + void disablePersistence() { + // persistence isn't enabled yet anyway (have to manually save things, + // defining the format and file etc) + } + + protected static BasicLocationDefinition localhost(String id) { + return new BasicLocationDefinition(id, "localhost", "localhost", null); + } + + /** to catch circular references */ + protected ThreadLocal<Set<String>> specsSeen = new ThreadLocal<Set<String>>(); + + @Override @Deprecated + public boolean canMaybeResolve(String spec) { + return getSpecResolver(spec) != null; + } + + @Override + public final Location resolve(String spec) { + return resolve(spec, true, null).get(); + } + + @Override @Deprecated + public final Location resolveIfPossible(String spec) { + if (!canMaybeResolve(spec)) return null; + return resolve(spec, null, null).orNull(); + } + + @Deprecated /** since 0.7.0 not used */ + public final Maybe<Location> resolve(String spec, boolean manage) { + return resolve(spec, manage, null); + } + + public Maybe<Location> resolve(String spec, Boolean manage, Map locationFlags) { + try { + locationFlags = MutableMap.copyOf(locationFlags); + if (manage!=null) { + locationFlags.put(LocalLocationManager.CREATE_UNMANAGED, !manage); + } + + Set<String> seenSoFar = specsSeen.get(); + if (seenSoFar==null) { + seenSoFar = new LinkedHashSet<String>(); + specsSeen.set(seenSoFar); + } + if (seenSoFar.contains(spec)) + return Maybe.absent(Suppliers.ofInstance(new IllegalStateException("Circular reference in definition of location '"+spec+"' ("+seenSoFar+")"))); + seenSoFar.add(spec); + + LocationResolver resolver = getSpecResolver(spec); + + if (resolver != null) { + try { + return Maybe.of(resolver.newLocationFromString(locationFlags, spec, this)); + } catch (RuntimeException e) { + return Maybe.absent(Suppliers.ofInstance(e)); + } + } + + // problem: but let's ensure that classpath is sane to give better errors in common IDE bogus case; + // and avoid repeated logging + String errmsg; + if (spec == null || specsWarnedOnException.add(spec)) { + if (resolvers.get("id")==null || resolvers.get("named")==null) { + log.error("Standard location resolvers not installed, location resolution will fail shortly. " + + "This usually indicates a classpath problem, such as when running from an IDE which " + + "has not properly copied META-INF/services from src/main/resources. " + + "Known resolvers are: "+resolvers.keySet()); + errmsg = "Unresolvable location '"+spec+"': " + + "Problem detected with location resolver configuration; " + + resolvers.keySet()+" are the only available location resolvers. " + + "More information can be found in the logs."; + } else { + log.debug("Location resolution failed for '"+spec+"' (if this is being loaded it will fail shortly): known resolvers are: "+resolvers.keySet()); + errmsg = "Unknown location '"+spec+"': " + + "either this location is not recognised or there is a problem with location resolver configuration."; + } + } else { + // For helpful log message construction: assumes classpath will not suddenly become wrong; might happen with OSGi though! + if (log.isDebugEnabled()) log.debug("Location resolution failed again for '"+spec+"' (throwing)"); + errmsg = "Unknown location '"+spec+"': " + + "either this location is not recognised or there is a problem with location resolver configuration."; + } + + return Maybe.absent(Suppliers.ofInstance(new NoSuchElementException(errmsg))); + + } finally { + specsSeen.remove(); + } + } + + @Override + public final Location resolve(String spec, Map locationFlags) { + return resolve(spec, null, locationFlags).get(); + } + + protected LocationResolver getSpecResolver(String spec) { + int colonIndex = spec.indexOf(':'); + int bracketIndex = spec.indexOf("("); + int dividerIndex = (colonIndex < 0) ? bracketIndex : (bracketIndex < 0 ? colonIndex : Math.min(bracketIndex, colonIndex)); + String prefix = dividerIndex >= 0 ? spec.substring(0, dividerIndex) : spec; + LocationResolver resolver = resolvers.get(prefix); + + if (resolver == null) + resolver = getSpecDefaultResolver(spec); + + return resolver; + } + + protected LocationResolver getSpecDefaultResolver(String spec) { + return getSpecFirstResolver(spec, "id", "named", "jclouds"); + } + protected LocationResolver getSpecFirstResolver(String spec, String ...resolversToCheck) { + for (String resolverId: resolversToCheck) { + LocationResolver resolver = resolvers.get(resolverId); + if (resolver!=null && resolver.accepts(spec, this)) + return resolver; + } + return null; + } + + /** providers default impl for RegistryLocationResolver.accepts */ + public static boolean isResolverPrefixForSpec(LocationResolver resolver, String spec, boolean argumentRequired) { + if (spec==null) return false; + if (spec.startsWith(resolver.getPrefix()+":")) return true; + if (!argumentRequired && spec.equals(resolver.getPrefix())) return true; + return false; + } + + @Override + public List<Location> resolve(Iterable<?> spec) { + List<Location> result = new ArrayList<Location>(); + for (Object id : spec) { + if (id instanceof String) { + result.add(resolve((String) id)); + } else if (id instanceof Location) { + result.add((Location) id); + } else { + if (id instanceof Iterable) + throw new IllegalArgumentException("Cannot resolve '"+id+"' to a location; collections of collections not allowed"); + throw new IllegalArgumentException("Cannot resolve '"+id+"' to a location; unsupported type "+ + (id == null ? "null" : id.getClass().getName())); + } + } + return result; + } + + public List<Location> resolveList(Object l) { + if (l==null) l = Collections.emptyList(); + if (l instanceof String) l = JavaStringEscapes.unwrapJsonishListIfPossible((String)l); + if (l instanceof Iterable) return resolve((Iterable<?>)l); + throw new IllegalArgumentException("Location list must be supplied as a collection or a string, not "+ + JavaClassNames.simpleClassName(l)+"/"+l); + } + + @Override + public Location resolve(LocationDefinition ld) { + return resolve(ld, null, null).get(); + } + + @Override @Deprecated + public Location resolveForPeeking(LocationDefinition ld) { + // TODO should clean up how locations are stored, figuring out whether they are shared or not; + // or maybe better, the API calls to this might just want to get the LocationSpec objects back + + // for now we use a 'CREATE_UNMANGED' flag to prevent management (leaks and logging) + return resolve(ld, ConfigBag.newInstance().configure(LocalLocationManager.CREATE_UNMANAGED, true).getAllConfig()); + } + + @Override @Deprecated + public Location resolve(LocationDefinition ld, Map<?,?> flags) { + return resolveLocationDefinition(ld, flags, null); + } + + /** @deprecated since 0.7.0 not used (and optionalName was ignored anyway) */ + @Deprecated + public Location resolveLocationDefinition(LocationDefinition ld, Map locationFlags, String optionalName) { + return resolve(ld, null, locationFlags).get(); + } + + public Maybe<Location> resolve(LocationDefinition ld, Boolean manage, Map locationFlags) { + ConfigBag newLocationFlags = ConfigBag.newInstance(ld.getConfig()) + .putAll(locationFlags) + .putIfAbsentAndNotNull(LocationInternal.NAMED_SPEC_NAME, ld.getName()) + .putIfAbsentAndNotNull(LocationInternal.ORIGINAL_SPEC, ld.getName()); + Maybe<Location> result = resolve(ld.getSpec(), manage, newLocationFlags.getAllConfigRaw()); + if (result.isPresent()) + return result; + throw new IllegalStateException("Cannot instantiate location '"+ld+"' pointing at "+ld.getSpec()+": "+ + Exceptions.collapseText( ((Absent<?>)result).getException() )); + } + + @Override + public Map getProperties() { + return mgmt.getConfig().asMapWithStringKeys(); + } + + @VisibleForTesting + public static void setupLocationRegistryForTesting(ManagementContext mgmt) { + // ensure localhost is added (even on windows) + LocationDefinition l = mgmt.getLocationRegistry().getDefinedLocationByName("localhost"); + if (l==null) mgmt.getLocationRegistry().updateDefinedLocation( + BasicLocationRegistry.localhost(Identifiers.makeRandomId(8)) ); + + ((BasicLocationRegistry)mgmt.getLocationRegistry()).disablePersistence(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/BasicMachineDetails.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/BasicMachineDetails.java b/core/src/main/java/org/apache/brooklyn/location/basic/BasicMachineDetails.java new file mode 100644 index 0000000..e4b4575 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/BasicMachineDetails.java @@ -0,0 +1,181 @@ +/* + * 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.location.basic; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; + +import org.apache.brooklyn.api.management.Task; +import org.apache.brooklyn.location.MachineDetails; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.brooklyn.location.HardwareDetails; +import org.apache.brooklyn.location.OsDetails; +import brooklyn.util.ResourceUtils; +import brooklyn.util.stream.Streams; +import brooklyn.util.task.DynamicTasks; +import brooklyn.util.task.TaskTags; +import brooklyn.util.task.ssh.internal.PlainSshExecTaskFactory; +import brooklyn.util.task.system.ProcessTaskWrapper; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Objects; +import com.google.common.base.Splitter; +import com.google.common.base.Throwables; +import com.google.common.collect.Maps; +import com.google.common.io.CharStreams; + +@Immutable +public class BasicMachineDetails implements MachineDetails { + + public static final Logger LOG = LoggerFactory.getLogger(BasicMachineDetails.class); + + private final HardwareDetails hardwareDetails; + private final OsDetails osDetails; + + public BasicMachineDetails(HardwareDetails hardwareDetails, OsDetails osDetails) { + this.hardwareDetails = checkNotNull(hardwareDetails, "hardwareDetails"); + this.osDetails = checkNotNull(osDetails, "osDetails"); + } + + @Nonnull + @Override + public HardwareDetails getHardwareDetails() { + return hardwareDetails; + } + + @Nonnull + @Override + public OsDetails getOsDetails() { + return osDetails; + } + + @Override + public String toString() { + return Objects.toStringHelper(MachineDetails.class) + .add("os", osDetails) + .add("hardware", hardwareDetails) + .toString(); + } + + /** + * Creates a MachineDetails for the given location by SSHing to the machine and + * running a Bash script to gather data. Should only be called from within a + * task context. If this might not be the case then use {@link + * #taskForSshMachineLocation(SshMachineLocation)} instead. + */ + static BasicMachineDetails forSshMachineLocation(SshMachineLocation location) { + return TaskTags.markInessential(DynamicTasks.queueIfPossible(taskForSshMachineLocation(location)) + .orSubmitAsync() + .asTask()) + .getUnchecked(); + } + + /** + * @return A task that gathers machine details by SSHing to the machine and running + * a Bash script to gather data. + */ + static Task<BasicMachineDetails> taskForSshMachineLocation(SshMachineLocation location) { + BufferedReader reader = new BufferedReader(Streams.reader( + new ResourceUtils(BasicMachineDetails.class).getResourceFromUrl( + "classpath://org/apache/brooklyn/location/basic/os-details.sh"))); + List<String> script; + try { + script = CharStreams.readLines(reader); + } catch (IOException e) { + LOG.error("Error reading os-details script", e); + throw Throwables.propagate(e); + } finally { + try { + reader.close(); + } catch (IOException e) { + // Not rethrowing e because it might obscure an exception caught by the first catch + LOG.error("Error closing os-details script reader", e); + } + } + Task<BasicMachineDetails> task = new PlainSshExecTaskFactory<String>(location, script) + .summary("Getting machine details for: " + location) + .requiringZeroAndReturningStdout() + .returning(taskToMachineDetailsFunction(location)) + .newTask() + .asTask(); + + return task; + } + + private static Function<ProcessTaskWrapper<?>, BasicMachineDetails> taskToMachineDetailsFunction(final SshMachineLocation location) { + return new Function<ProcessTaskWrapper<?>, BasicMachineDetails>() { + @Override + public BasicMachineDetails apply(ProcessTaskWrapper<?> input) { + if (input.getExitCode() != 0) { + LOG.warn("Non-zero exit code when fetching machine details for {}; guessing anonymous linux", location); + return new BasicMachineDetails(new BasicHardwareDetails(null, null), + BasicOsDetails.Factory.ANONYMOUS_LINUX); + } + + String stdout = input.getStdout(); + if (LOG.isDebugEnabled()) { + LOG.debug("Found following details at {}: {}", location, stdout); + } + + Map<String,String> details = Maps.newHashMap(Splitter.on(CharMatcher.anyOf("\r\n")) + .omitEmptyStrings() + .withKeyValueSeparator(":") + .split(stdout)); + + String name = details.remove("name"); + String version = details.remove("version"); + String architecture = details.remove("architecture"); + Integer ram = intOrNull(details, "ram"); + Integer cpuCount = intOrNull(details, "cpus"); + if (!details.isEmpty()) { + LOG.debug("Unused keys from os-details script: " + Joiner.on(", ").join(details.keySet())); + } + + OsDetails osDetails = new BasicOsDetails(name, architecture, version); + HardwareDetails hardwareDetails = new BasicHardwareDetails(cpuCount, ram); + BasicMachineDetails machineDetails = new BasicMachineDetails(hardwareDetails, osDetails); + + if (LOG.isDebugEnabled()) + LOG.debug("Machine details for {}: {}", location, machineDetails); + + return machineDetails; + } + + private Integer intOrNull(Map<String, String> details, String key) { + try { + return Integer.valueOf(details.remove(key)); + } catch (NumberFormatException e) { + return null; + } + } + }; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/BasicMachineMetadata.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/BasicMachineMetadata.java b/core/src/main/java/org/apache/brooklyn/location/basic/BasicMachineMetadata.java new file mode 100644 index 0000000..ff1c0af --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/BasicMachineMetadata.java @@ -0,0 +1,83 @@ +/* + * 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.location.basic; + +import com.google.common.base.Objects; +import org.apache.brooklyn.location.MachineManagementMixins; + +public class BasicMachineMetadata implements MachineManagementMixins.MachineMetadata { + + final String id, name, primaryIp; + final Boolean isRunning; + final Object originalMetadata; + + public BasicMachineMetadata(String id, String name, String primaryIp, Boolean isRunning, Object originalMetadata) { + super(); + this.id = id; + this.name = name; + this.primaryIp = primaryIp; + this.isRunning = isRunning; + this.originalMetadata = originalMetadata; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getPrimaryIp() { + return primaryIp; + } + + public Boolean isRunning() { + return isRunning; + } + + public Object getOriginalMetadata() { + return originalMetadata; + } + + @Override + public int hashCode() { + return Objects.hashCode(id, isRunning, name, originalMetadata, primaryIp); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + BasicMachineMetadata other = (BasicMachineMetadata) obj; + if (!Objects.equal(id, other.id)) return false; + if (!Objects.equal(name, other.name)) return false; + if (!Objects.equal(primaryIp, other.primaryIp)) return false; + if (!Objects.equal(isRunning, other.isRunning)) return false; + if (!Objects.equal(originalMetadata, other.originalMetadata)) return false; + return true; + } + + @Override + public String toString() { + return Objects.toStringHelper(this).add("id", id).add("name", name).add("originalMetadata", originalMetadata).toString(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/e2c57058/core/src/main/java/org/apache/brooklyn/location/basic/BasicOsDetails.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/basic/BasicOsDetails.java b/core/src/main/java/org/apache/brooklyn/location/basic/BasicOsDetails.java new file mode 100644 index 0000000..364e5f3 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/basic/BasicOsDetails.java @@ -0,0 +1,122 @@ +/* + * 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.location.basic; + +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import com.google.common.base.Objects; + +import org.apache.brooklyn.location.OsDetails; + +@Immutable +public class BasicOsDetails implements OsDetails { + + final String name, arch, version; + final boolean is64bit; + // (?i) forces matches to be case insensitive + public static final String UNIX_OS_NAME_PATTERNS = "(?i).*linux.*|centos|debian|fedora|gentoo|rhel|slackware|solaris|suse|ubuntu|coreos"; + + /** Sets is64Bit according to value of arch parameter. */ + public BasicOsDetails(String name, String arch, String version) { + this(name, arch, version, arch != null && arch.contains("64")); + } + + public BasicOsDetails(String name, String arch, String version, boolean is64Bit) { + this.name = name; this.arch = arch; this.version = version; this.is64bit = is64Bit; + } + + // TODO: Should be replaced with an enum like Jclouds' OsFamily and isX methods should + // switch against known cases + @Nullable + @Override + public String getName() { + return name; + } + + @Nullable + @Override + public String getArch() { + return arch; + } + + @Nullable + @Override + public String getVersion() { + return version; + } + + @Override + public boolean isWindows() { + //TODO confirm + return getName()!=null && getName().toLowerCase().contains("microsoft"); + } + + @Override + public boolean isLinux() { + return getName() != null && Pattern.matches(UNIX_OS_NAME_PATTERNS, getName()); + } + + @Override + public boolean isMac() { + return getName()!=null && getName().equals(OsNames.MAC_OS_X); + } + + @Override + public boolean is64bit() { + return is64bit; + } + + @Override + public String toString() { + return Objects.toStringHelper(OsDetails.class) + .omitNullValues() + .add("name", name) + .add("version", version) + .add("arch", arch) + .toString(); + } + + public static class OsNames { + public static final String MAC_OS_X = "Mac OS X"; + } + + public static class OsArchs { + public static final String X_86_64 = "x86_64"; +// public static final String X_86 = "x86"; +// // is this standard? or do we ever need the above? + public static final String I386 = "i386"; + } + + public static class OsVersions { + public static final String MAC_10_8 = "10.8"; + public static final String MAC_10_9 = "10.9"; + } + + public static class Factory { + public static OsDetails newLocalhostInstance() { + return new BasicOsDetails(System.getProperty("os.name"), System.getProperty("os.arch"), System.getProperty("os.version")); + } + + public static final OsDetails ANONYMOUS_LINUX = new BasicOsDetails("linux", OsArchs.I386, "unknown"); + public static final OsDetails ANONYMOUS_LINUX_64 = new BasicOsDetails("linux", OsArchs.X_86_64, "unknown"); + } + +}
