http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/BasicLocationRegistry.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/BasicLocationRegistry.java b/core/src/main/java/org/apache/brooklyn/location/core/BasicLocationRegistry.java new file mode 100644 index 0000000..329c38e --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/BasicLocationRegistry.java @@ -0,0 +1,489 @@ +/* + * 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.core; + +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.api.catalog.BrooklynCatalog; +import org.apache.brooklyn.api.catalog.CatalogItem; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.location.LocationDefinition; +import org.apache.brooklyn.api.location.LocationRegistry; +import org.apache.brooklyn.api.location.LocationResolver; +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.config.ConfigMap; +import org.apache.brooklyn.core.catalog.CatalogPredicates; +import org.apache.brooklyn.core.config.ConfigPredicates; +import org.apache.brooklyn.core.config.ConfigUtils; +import org.apache.brooklyn.core.mgmt.internal.LocalLocationManager; +import org.apache.brooklyn.location.core.internal.LocationInternal; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.guava.Maybe.Absent; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.text.Identifiers; +import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; +import org.apache.brooklyn.util.text.WildcardGlobs; +import org.apache.brooklyn.util.text.WildcardGlobs.PhraseTreatment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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()); + MutableList<LocationResolver> loadedResolvers; + try { + loadedResolvers = MutableList.copyOf(loader); + } catch (Throwable e) { + log.warn("Error loading resolvers (rethrowing): "+e); + throw Exceptions.propagate(e); + } + + for (LocationResolver r: loadedResolvers) { + 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/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/BasicMachineDetails.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/BasicMachineDetails.java b/core/src/main/java/org/apache/brooklyn/location/core/BasicMachineDetails.java new file mode 100644 index 0000000..0c323ed --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/BasicMachineDetails.java @@ -0,0 +1,183 @@ +/* + * 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.core; + +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.location.HardwareDetails; +import org.apache.brooklyn.api.location.MachineDetails; +import org.apache.brooklyn.api.location.OsDetails; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.location.ssh.SshMachineLocation; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.task.DynamicTasks; +import org.apache.brooklyn.util.core.task.TaskTags; +import org.apache.brooklyn.util.core.task.ssh.internal.PlainSshExecTaskFactory; +import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; +import org.apache.brooklyn.util.stream.Streams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.Beta; +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. + */ + @Beta + public static BasicMachineDetails forSshMachineLocationLive(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. + */ + public 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/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/BasicMachineMetadata.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/BasicMachineMetadata.java b/core/src/main/java/org/apache/brooklyn/location/core/BasicMachineMetadata.java new file mode 100644 index 0000000..37b61ee --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/BasicMachineMetadata.java @@ -0,0 +1,84 @@ +/* + * 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.core; + +import com.google.common.base.Objects; + +import org.apache.brooklyn.api.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/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/BasicOsDetails.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/BasicOsDetails.java b/core/src/main/java/org/apache/brooklyn/location/core/BasicOsDetails.java new file mode 100644 index 0000000..089df03 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/BasicOsDetails.java @@ -0,0 +1,123 @@ +/* + * 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.core; + +import java.util.regex.Pattern; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import com.google.common.base.Objects; + +import org.apache.brooklyn.api.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"); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/CatalogLocationResolver.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/CatalogLocationResolver.java b/core/src/main/java/org/apache/brooklyn/location/core/CatalogLocationResolver.java new file mode 100644 index 0000000..a907a49 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/CatalogLocationResolver.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.location.core; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.api.catalog.CatalogItem; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.location.LocationRegistry; +import org.apache.brooklyn.api.location.LocationResolver; +import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.catalog.internal.CatalogUtils; + +/** + * Given a location spec in the form {@code brooklyn.catalog:<symbolicName>:<version>}, + * looks up the catalog to get its definition and creates such a location. + */ +public class CatalogLocationResolver implements LocationResolver { + + @SuppressWarnings("unused") + private static final Logger log = LoggerFactory.getLogger(CatalogLocationResolver.class); + + public static final String NAME = "brooklyn.catalog"; + + private ManagementContext managementContext; + + @Override + public void init(ManagementContext managementContext) { + this.managementContext = checkNotNull(managementContext, "managementContext"); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Location newLocationFromString(Map locationFlags, String spec, LocationRegistry registry) { + String id = spec.substring(NAME.length()+1); + CatalogItem<?, ?> item = CatalogUtils.getCatalogItemOptionalVersion(managementContext, id); + LocationSpec<?> origLocSpec = managementContext.getCatalog().createSpec((CatalogItem<Location, LocationSpec<?>>)item); + LocationSpec<?> locSpec = LocationSpec.create(origLocSpec) + .configure(locationFlags); + return managementContext.getLocationManager().createLocation(locSpec); + } + + @Override + public String getPrefix() { + return NAME; + } + + /** + * accepts anything that looks like it will be a YAML catalog item (e.g. starting "brooklyn.locations") + */ + @Override + public boolean accepts(String spec, LocationRegistry registry) { + if (BasicLocationRegistry.isResolverPrefixForSpec(this, spec, false)) return true; + if (registry.getDefinedLocationByName(spec)!=null) return true; + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/DefinedLocationByIdResolver.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/DefinedLocationByIdResolver.java b/core/src/main/java/org/apache/brooklyn/location/core/DefinedLocationByIdResolver.java new file mode 100644 index 0000000..efe0aff --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/DefinedLocationByIdResolver.java @@ -0,0 +1,74 @@ +/* + * 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.core; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; + +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.location.LocationDefinition; +import org.apache.brooklyn.api.location.LocationRegistry; +import org.apache.brooklyn.api.location.LocationResolver; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * looks up based on ID in DefinedLocations map + */ +public class DefinedLocationByIdResolver implements LocationResolver { + + public static final Logger log = LoggerFactory.getLogger(DefinedLocationByIdResolver.class); + + public static final String ID = "id"; + + private volatile ManagementContext managementContext; + + @Override + public void init(ManagementContext managementContext) { + this.managementContext = checkNotNull(managementContext, "managementContext"); + } + + @SuppressWarnings({ "rawtypes" }) + @Override + public Location newLocationFromString(Map locationFlags, String spec, LocationRegistry registry) { + String id = spec; + if (spec.toLowerCase().startsWith(ID+":")) { + id = spec.substring( (ID+":").length() ); + } + LocationDefinition ld = registry.getDefinedLocationById(id); + ld.getSpec(); + return ((BasicLocationRegistry)registry).resolveLocationDefinition(ld, locationFlags, null); + } + + @Override + public String getPrefix() { + return ID; + } + + /** accepts anything starting id:xxx or just xxx where xxx is a defined location ID */ + @Override + public boolean accepts(String spec, LocationRegistry registry) { + if (BasicLocationRegistry.isResolverPrefixForSpec(this, spec, false)) return true; + if (registry.getDefinedLocationById(spec)!=null) return true; + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/DeprecatedKeysMappingBuilder.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/DeprecatedKeysMappingBuilder.java b/core/src/main/java/org/apache/brooklyn/location/core/DeprecatedKeysMappingBuilder.java new file mode 100644 index 0000000..ad98f04 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/DeprecatedKeysMappingBuilder.java @@ -0,0 +1,66 @@ +/* + * 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.core; + +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey; +import org.slf4j.Logger; + +import com.google.common.base.CaseFormat; +import com.google.common.collect.ImmutableMap; + +/** +* @deprecated since 0.6; for use only in converting deprecated flags; will be deleted in future version. +*/ +public class DeprecatedKeysMappingBuilder { + private final ImmutableMap.Builder<String,String> builder = new ImmutableMap.Builder<String,String>(); + private final Logger logger; + + public DeprecatedKeysMappingBuilder(Logger logger) { + this.logger = logger; + } + + public DeprecatedKeysMappingBuilder camelToHyphen(ConfigKey<?> key) { + return camelToHyphen(key.getName()); + } + + public DeprecatedKeysMappingBuilder camelToHyphen(String key) { + String hyphen = toHyphen(key); + if (key.equals(hyphen)) { + logger.warn("Invalid attempt to convert camel-case key {} to deprecated hyphen-case: both the same", hyphen); + } else { + builder.put(hyphen, key); + } + return this; + } + + public DeprecatedKeysMappingBuilder putAll(Map<String,String> vals) { + builder.putAll(vals); + return this; + } + + public Map<String,String> build() { + return builder.build(); + } + + private String toHyphen(String word) { + return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, word); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/HasSubnetHostname.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/HasSubnetHostname.java b/core/src/main/java/org/apache/brooklyn/location/core/HasSubnetHostname.java new file mode 100644 index 0000000..604d5ef --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/HasSubnetHostname.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.location.core; + +import com.google.common.annotations.Beta; + +@Beta +public interface HasSubnetHostname { + + /** returns a hostname for use internally within a subnet / VPC */ + @Beta + String getSubnetHostname(); + + /** returns an IP for use internally within a subnet / VPC */ + String getSubnetIp(); +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/LocationConfigKeys.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/LocationConfigKeys.java b/core/src/main/java/org/apache/brooklyn/location/core/LocationConfigKeys.java new file mode 100644 index 0000000..689a1d7 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/LocationConfigKeys.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.location.core; + +import java.io.File; +import java.util.Set; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.BasicConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.util.os.Os; + +import com.google.common.base.CaseFormat; +import com.google.common.reflect.TypeToken; + +public class LocationConfigKeys { + + public static final ConfigKey<String> LOCATION_ID = ConfigKeys.newStringConfigKey("id"); + public static final ConfigKey<String> DISPLAY_NAME = ConfigKeys.newStringConfigKey("displayName"); + public static final ConfigKey<Boolean> ENABLED = ConfigKeys.newBooleanConfigKey("enabled", "Whether the location is enabled for listing and use " + + "(only supported for selected locations)", true); + + public static final ConfigKey<String> ACCESS_IDENTITY = ConfigKeys.newStringConfigKey("identity"); + public static final ConfigKey<String> ACCESS_CREDENTIAL = ConfigKeys.newStringConfigKey("credential"); + + public static final ConfigKey<Double> LATITUDE = new BasicConfigKey<Double>(Double.class, "latitude"); + public static final ConfigKey<Double> LONGITUDE = new BasicConfigKey<Double>(Double.class, "longitude"); + + public static final ConfigKey<String> CLOUD_PROVIDER = ConfigKeys.newStringConfigKey("provider"); + public static final ConfigKey<String> CLOUD_ENDPOINT = ConfigKeys.newStringConfigKey("endpoint"); + public static final ConfigKey<String> CLOUD_REGION_ID = ConfigKeys.newStringConfigKey("region"); + public static final ConfigKey<String> CLOUD_AVAILABILITY_ZONE_ID = ConfigKeys.newStringConfigKey("availabilityZone"); + + @SuppressWarnings("serial") + public static final ConfigKey<Set<String>> ISO_3166 = ConfigKeys.newConfigKey(new TypeToken<Set<String>>() {}, "iso3166", "ISO-3166 or ISO-3166-2 location codes"); + + public static final ConfigKey<String> USER = ConfigKeys.newStringConfigKey("user", + "user account for normal access to the remote machine, defaulting to local user", System.getProperty("user.name")); + + public static final ConfigKey<String> PASSWORD = ConfigKeys.newStringConfigKey("password", "password to use for ssh; note some images do not allow password-based ssh access"); + public static final ConfigKey<String> PUBLIC_KEY_FILE = ConfigKeys.newStringConfigKey("publicKeyFile", "ssh public key file to use; if blank will infer from privateKeyFile by appending \".pub\""); + public static final ConfigKey<String> PUBLIC_KEY_DATA = ConfigKeys.newStringConfigKey("publicKeyData", "ssh public key string to use (takes precedence over publicKeyFile)"); + public static final ConfigKey<String> PRIVATE_KEY_FILE = ConfigKeys.newStringConfigKey("privateKeyFile", "a '" + File.pathSeparator + "' separated list of ssh private key files; uses first in list that can be read", + Os.fromHome(".ssh/id_rsa") + File.pathSeparator + Os.fromHome(".ssh/id_dsa")); + public static final ConfigKey<String> PRIVATE_KEY_DATA = ConfigKeys.newStringConfigKey("privateKeyData", "ssh private key string to use (takes precedence over privateKeyFile)"); + public static final ConfigKey<String> PRIVATE_KEY_PASSPHRASE = ConfigKeys.newStringConfigKey("privateKeyPassphrase"); + + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PUBLIC_KEY_FILE = ConfigKeys.convert(PUBLIC_KEY_FILE, CaseFormat.LOWER_CAMEL, CaseFormat.LOWER_HYPHEN); + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PUBLIC_KEY_DATA = ConfigKeys.convert(PUBLIC_KEY_DATA, CaseFormat.LOWER_CAMEL, CaseFormat.LOWER_HYPHEN); + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PRIVATE_KEY_FILE = ConfigKeys.convert(PRIVATE_KEY_FILE, CaseFormat.LOWER_CAMEL, CaseFormat.LOWER_HYPHEN); + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PRIVATE_KEY_DATA = ConfigKeys.convert(PRIVATE_KEY_DATA, CaseFormat.LOWER_CAMEL, CaseFormat.LOWER_HYPHEN); + /** @deprecated since 0.6.0; included here so it gets picked up in auto-detect routines */ @Deprecated + public static final ConfigKey<String> LEGACY_PRIVATE_KEY_PASSPHRASE = ConfigKeys.convert(PRIVATE_KEY_PASSPHRASE, CaseFormat.LOWER_CAMEL, CaseFormat.LOWER_HYPHEN); + + public static final ConfigKey<Object> CALLER_CONTEXT = new BasicConfigKey<Object>(Object.class, "callerContext", + "An object whose toString is used for logging, to indicate wherefore a VM is being created"); + public static final ConfigKey<String> CLOUD_MACHINE_NAMER_CLASS = ConfigKeys.newStringConfigKey("cloudMachineNamer", "fully qualified class name of a class that extends CloudMachineNamer and has a single-parameter constructor that takes a ConfigBag"); + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1ad34d7/core/src/main/java/org/apache/brooklyn/location/core/LocationConfigUtils.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/location/core/LocationConfigUtils.java b/core/src/main/java/org/apache/brooklyn/location/core/LocationConfigUtils.java new file mode 100644 index 0000000..80879b9 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/location/core/LocationConfigUtils.java @@ -0,0 +1,559 @@ +/* + * 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.core; + +import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.internal.BrooklynFeatureEnablement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.brooklyn.location.cloud.CloudLocationConfig; +import org.apache.brooklyn.location.core.internal.LocationInternal; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.crypto.SecureKeys; +import org.apache.brooklyn.util.core.crypto.SecureKeys.PassphraseProblem; +import org.apache.brooklyn.util.crypto.AuthorizedKeysParser; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.os.Os; +import org.apache.brooklyn.util.text.StringFunctions; +import org.apache.brooklyn.util.text.Strings; + +import com.google.common.annotations.Beta; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +public class LocationConfigUtils { + + private static final Logger log = LoggerFactory.getLogger(LocationConfigUtils.class); + + /** Creates an instance of {@link OsCredential} by inspecting {@link LocationConfigKeys#PASSWORD}; + * {@link LocationConfigKeys#PRIVATE_KEY_DATA} and {@link LocationConfigKeys#PRIVATE_KEY_FILE}; + * {@link LocationConfigKeys#PRIVATE_KEY_PASSPHRASE} if needed, and + * {@link LocationConfigKeys#PRIVATE_KEY_DATA} and {@link LocationConfigKeys#PRIVATE_KEY_FILE} + * (defaulting to the private key file + ".pub"). + **/ + public static OsCredential getOsCredential(ConfigBag config) { + return OsCredential.newInstance(config); + } + + /** Convenience class for holding private/public keys and passwords, inferring from config keys. + * See {@link LocationConfigUtils#getOsCredential(ConfigBag)}. */ + @Beta // would be nice to replace with a builder pattern + public static class OsCredential { + private final ConfigBag config; + private boolean preferPassword = false; + private boolean tryDefaultKeys = true; + private boolean requirePublicKey = true; + private boolean doKeyValidation = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_VALIDATE_LOCATION_SSH_KEYS); + private boolean warnOnErrors = true; + private boolean throwOnErrors = false; + + private boolean dirty = true;; + + private String privateKeyData; + private String publicKeyData; + private String password; + + private OsCredential(ConfigBag config) { + this.config = config; + } + + /** throws if there are any problems */ + public OsCredential checkNotEmpty() { + checkNoErrors(); + + if (!hasKey() && !hasPassword()) { + if (warningMessages.size()>0) + throw new IllegalStateException("Could not find credentials: "+warningMessages); + else + throw new IllegalStateException("Could not find credentials"); + } + return this; + } + + /** throws if there were errors resolving (e.g. explicit keys, none of which were found/valid, or public key required and not found) + * @return */ + public OsCredential checkNoErrors() { + throwOnErrors(true); + dirty(); + infer(); + return this; + } + + public OsCredential logAnyWarnings() { + if (!warningMessages.isEmpty()) + log.warn("When reading credentials: "+warningMessages); + return this; + } + + public Set<String> getWarningMessages() { + return warningMessages; + } + + /** returns either the key or password or null; if both a key and a password this prefers the key unless otherwise set + * via {@link #preferPassword()} */ + public synchronized String getPreferredCredential() { + infer(); + + if (isUsingPassword()) return password; + if (hasKey()) return privateKeyData; + return null; + } + + /** if there is no credential (ignores public key) */ + public boolean isEmpty() { + return !hasKey() && !hasPassword(); + } + public boolean hasKey() { + infer(); + // key has stricter non-blank check than password + return Strings.isNonBlank(privateKeyData); + } + public boolean hasPassword() { + infer(); + // blank, even empty passwords are allowed + return password!=null; + } + /** if a password is available, and either this is preferred over a key or there is no key */ + public boolean isUsingPassword() { + return hasPassword() && (!hasKey() || preferPassword); + } + + public String getPrivateKeyData() { + infer(); + return privateKeyData; + } + public String getPublicKeyData() { + infer(); + return publicKeyData; + } + public String getPassword() { + infer(); + return password; + } + + /** if both key and password supplied, prefer the key; the default */ + public OsCredential preferKey() { preferPassword = false; return dirty(); } + /** if both key and password supplied, prefer the password; see {@link #preferKey()} */ + public OsCredential preferPassword() { preferPassword = true; return dirty(); } + + /** if false, do not mind if there is no public key corresponding to any private key; + * defaults to true; only applies if a private key is set */ + public OsCredential requirePublicKey(boolean requirePublicKey) { + this.requirePublicKey = requirePublicKey; + return dirty(); + } + /** whether to check the private/public keys and passphrase are coherent; default true */ + public OsCredential doKeyValidation(boolean doKeyValidation) { + this.doKeyValidation = doKeyValidation; + return dirty(); + } + /** if true (the default) this will look at default locations set on keys */ + public OsCredential useDefaultKeys(boolean tryDefaultKeys) { + this.tryDefaultKeys = tryDefaultKeys; + return dirty(); + } + /** whether to log warnings on problems */ + public OsCredential warnOnErrors(boolean warnOnErrors) { + this.warnOnErrors = warnOnErrors; + return dirty(); + } + /** whether to throw on problems */ + public OsCredential throwOnErrors(boolean throwOnErrors) { + this.throwOnErrors = throwOnErrors; + return dirty(); + } + + private OsCredential dirty() { dirty = true; return this; } + + public static OsCredential newInstance(ConfigBag config) { + return new OsCredential(config); + } + + private synchronized void infer() { + if (!dirty) return; + warningMessages.clear(); + + log.debug("Inferring OS credentials"); + privateKeyData = config.get(LocationConfigKeys.PRIVATE_KEY_DATA); + password = config.get(LocationConfigKeys.PASSWORD); + publicKeyData = getKeyDataFromDataKeyOrFileKey(config, LocationConfigKeys.PUBLIC_KEY_DATA, LocationConfigKeys.PUBLIC_KEY_FILE); + + KeyPair privateKey = null; + + if (Strings.isBlank(privateKeyData)) { + // look up private key files + String privateKeyFiles = null; + boolean privateKeyFilesExplicitlySet = config.containsKey(LocationConfigKeys.PRIVATE_KEY_FILE); + if (privateKeyFilesExplicitlySet || (tryDefaultKeys && password==null)) + privateKeyFiles = config.get(LocationConfigKeys.PRIVATE_KEY_FILE); + if (Strings.isNonBlank(privateKeyFiles)) { + Iterator<String> fi = Arrays.asList(privateKeyFiles.split(File.pathSeparator)).iterator(); + while (fi.hasNext()) { + String file = fi.next(); + if (Strings.isNonBlank(file)) { + try { + // real URL's won't actual work, due to use of path separator above + // not real important, but we get it for free if "files" is a list instead. + // using ResourceUtils is useful for classpath resources + if (file!=null) + privateKeyData = ResourceUtils.create().getResourceAsString(file); + // else use data already set + + privateKey = getValidatedPrivateKey(file); + + if (privateKeyData==null) { + // was cleared due to validation error + } else if (Strings.isNonBlank(publicKeyData)) { + log.debug("Loaded private key data from "+file+" (public key data explicitly set)"); + break; + } else { + String publicKeyFile = (file!=null ? file+".pub" : "(data)"); + try { + publicKeyData = ResourceUtils.create().getResourceAsString(publicKeyFile); + + log.debug("Loaded private key data from "+file+ + " and public key data from "+publicKeyFile); + break; + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.debug("No public key file "+publicKeyFile+"; will try extracting from private key"); + publicKeyData = AuthorizedKeysParser.encodePublicKey(privateKey.getPublic()); + + if (publicKeyData==null) { + if (requirePublicKey) { + addWarning("Unable to find or extract public key for "+file, "skipping"); + } else { + log.debug("Loaded private key data from "+file+" (public key data not found but not required)"); + break; + } + } else { + log.debug("Loaded private key data from "+file+" (public key data extracted)"); + break; + } + privateKeyData = null; + } + } + + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + String message = "Missing/invalid private key file "+file; + if (privateKeyFilesExplicitlySet) addWarning(message, (!fi.hasNext() ? "no more files to try" : "trying next file")+": "+e); + } + } + } + if (privateKeyFilesExplicitlySet && Strings.isBlank(privateKeyData)) + error("No valid private keys found", ""+warningMessages); + } + } else { + privateKey = getValidatedPrivateKey("(data)"); + } + + if (privateKeyData!=null) { + if (requirePublicKey && Strings.isBlank(publicKeyData)) { + if (privateKey!=null) { + publicKeyData = AuthorizedKeysParser.encodePublicKey(privateKey.getPublic()); + } + if (Strings.isBlank(publicKeyData)) { + error("If explicit "+LocationConfigKeys.PRIVATE_KEY_DATA.getName()+" is supplied, then " + + "the corresponding "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" must also be supplied.", null); + } else { + log.debug("Public key data extracted"); + } + } + if (doKeyValidation && privateKey!=null && privateKey.getPublic()!=null && Strings.isNonBlank(publicKeyData)) { + PublicKey decoded = null; + try { + decoded = AuthorizedKeysParser.decodePublicKey(publicKeyData); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + addWarning("Invalid public key: "+decoded); + } + if (decoded!=null && !privateKey.getPublic().equals( decoded )) { + error("Public key inferred from does not match public key extracted from private key", null); + } + } + } + + log.debug("OS credential inference: "+this); + dirty = false; + } + + private KeyPair getValidatedPrivateKey(String label) { + KeyPair privateKey = null; + String passphrase = config.get(CloudLocationConfig.PRIVATE_KEY_PASSPHRASE); + try { + privateKey = SecureKeys.readPem(new ByteArrayInputStream(privateKeyData.getBytes()), passphrase); + if (passphrase!=null) { + // get the unencrypted key data for our internal use (jclouds requires this) + privateKeyData = SecureKeys.toPem(privateKey); + } + } catch (PassphraseProblem e) { + if (doKeyValidation) { + log.debug("Encountered error handling key "+label+": "+e, e); + if (Strings.isBlank(passphrase)) + addWarning("Passphrase required for key '"+label+"'"); + else + addWarning("Invalid passphrase for key '"+label+"'"); + privateKeyData = null; + } + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (doKeyValidation) { + addWarning("Unable to parse private key from '"+label+"': unknown format"); + privateKeyData = null; + } + } + return privateKey; + } + + Set<String> warningMessages = MutableSet.of(); + + private void error(String msg, String logExtension) { + addWarning(msg); + if (warnOnErrors) log.warn(msg+(logExtension==null ? "" : ": "+logExtension)); + if (throwOnErrors) throw new IllegalStateException(msg+(logExtension==null ? "" : "; "+logExtension)); + } + + private void addWarning(String msg) { + addWarning(msg, null); + } + private void addWarning(String msg, String debugExtension) { + log.debug(msg+(debugExtension==null ? "" : "; "+debugExtension)); + warningMessages.add(msg); + } + + @Override + public String toString() { + return getClass().getSimpleName()+"["+ + (Strings.isNonBlank(publicKeyData) ? publicKeyData : "no-public-key")+";"+ + (Strings.isNonBlank(privateKeyData) ? "private-key-present" : "no-private-key")+","+ + (password!=null ? "password(len="+password.length()+")" : "no-password")+"]"; + } + } + + /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated + public static String getPrivateKeyData(ConfigBag config) { + return getKeyData(config, LocationConfigKeys.PRIVATE_KEY_DATA, LocationConfigKeys.PRIVATE_KEY_FILE); + } + + /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated + public static String getPublicKeyData(ConfigBag config) { + String data = getKeyData(config, LocationConfigKeys.PUBLIC_KEY_DATA, LocationConfigKeys.PUBLIC_KEY_FILE); + if (groovyTruth(data)) return data; + + String privateKeyFile = config.get(LocationConfigKeys.PRIVATE_KEY_FILE); + if (groovyTruth(privateKeyFile)) { + List<String> privateKeyFiles = Arrays.asList(privateKeyFile.split(File.pathSeparator)); + List<String> publicKeyFiles = ImmutableList.copyOf(Iterables.transform(privateKeyFiles, StringFunctions.append(".pub"))); + List<String> publicKeyFilesTidied = tidyFilePaths(publicKeyFiles); + + String fileData = getFileContents(publicKeyFilesTidied); + if (groovyTruth(fileData)) { + if (log.isDebugEnabled()) log.debug("Loaded "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" from inferred files, based on "+LocationConfigKeys.PRIVATE_KEY_FILE.getName() + ": used " + publicKeyFilesTidied + " for "+config.getDescription()); + config.put(LocationConfigKeys.PUBLIC_KEY_DATA, fileData); + return fileData; + } else { + log.info("Not able to load "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" from inferred files, based on "+LocationConfigKeys.PRIVATE_KEY_FILE.getName() + ": tried " + publicKeyFilesTidied + " for "+config.getDescription()); + } + } + + return null; + } + + /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated + public static String getKeyData(ConfigBag config, ConfigKey<String> dataKey, ConfigKey<String> fileKey) { + return getKeyDataFromDataKeyOrFileKey(config, dataKey, fileKey); + } + + private static String getKeyDataFromDataKeyOrFileKey(ConfigBag config, ConfigKey<String> dataKey, ConfigKey<String> fileKey) { + boolean unused = config.isUnused(dataKey); + String data = config.get(dataKey); + if (groovyTruth(data) && !unused) { + return data; + } + + String file = config.get(fileKey); + if (groovyTruth(file)) { + List<String> files = Arrays.asList(file.split(File.pathSeparator)); + List<String> filesTidied = tidyFilePaths(files); + String fileData = getFileContents(filesTidied); + if (fileData == null) { + log.warn("Invalid file" + (files.size() > 1 ? "s" : "") + " for " + fileKey + " (given " + files + + (files.equals(filesTidied) ? "" : "; converted to " + filesTidied) + ") " + + "may fail provisioning " + config.getDescription()); + } else if (groovyTruth(data)) { + if (!fileData.trim().equals(data.trim())) + log.warn(dataKey.getName()+" and "+fileKey.getName()+" both specified; preferring the former"); + } else { + data = fileData; + config.put(dataKey, data); + config.get(dataKey); + } + } + + return data; + } + + /** + * Reads the given file(s) in-order, returning the contents of the first file that can be read. + * Returns the file contents, or null if none of the files can be read. + * + * @param files list of file paths + */ + private static String getFileContents(Iterable<String> files) { + Iterator<String> fi = files.iterator(); + while (fi.hasNext()) { + String file = fi.next(); + if (groovyTruth(file)) { + try { + // see comment above + String result = ResourceUtils.create().getResourceAsString(file); + if (result!=null) return result; + log.debug("Invalid file "+file+" ; " + (!fi.hasNext() ? "no more files to try" : "trying next file")+" (null)"); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.debug("Invalid file "+file+" ; " + (!fi.hasNext() ? "no more files to try" : "trying next file"), e); + } + } + } + return null; + } + + private static List<String> tidyFilePaths(Iterable<String> files) { + List<String> result = Lists.newArrayList(); + for (String file : files) { + result.add(Os.tidyPath(file)); + } + return result; + } + + /** @deprecated since 0.6.0 use configBag.getWithDeprecation */ + @Deprecated + @SuppressWarnings("unchecked") + public static <T> T getConfigCheckingDeprecatedAlternatives(ConfigBag configBag, ConfigKey<T> preferredKey, + ConfigKey<?> ...deprecatedKeys) { + T value1 = (T) configBag.getWithDeprecation(preferredKey, deprecatedKeys); + T value2 = getConfigCheckingDeprecatedAlternativesInternal(configBag, preferredKey, deprecatedKeys); + if (!Objects.equal(value1, value2)) { + // points to a bug in one of the get-with-deprecation methods + log.warn("Deprecated getConfig with deprecated keys "+Arrays.toString(deprecatedKeys)+" gets different value with " + + "new strategy "+preferredKey+" ("+value1+") and old ("+value2+"); preferring old value for now, but this behaviour will change"); + return value2; + } + return value1; + } + + @SuppressWarnings("unchecked") + private static <T> T getConfigCheckingDeprecatedAlternativesInternal(ConfigBag configBag, ConfigKey<T> preferredKey, + ConfigKey<?> ...deprecatedKeys) { + ConfigKey<?> keyProvidingValue = null; + T value = null; + boolean found = false; + if (configBag.containsKey(preferredKey)) { + value = configBag.get(preferredKey); + found = true; + keyProvidingValue = preferredKey; + } + + for (ConfigKey<?> deprecatedKey: deprecatedKeys) { + T altValue = null; + boolean altFound = false; + if (configBag.containsKey(deprecatedKey)) { + altValue = (T) configBag.get(deprecatedKey); + altFound = true; + + if (altFound) { + if (found) { + if (Objects.equal(value, altValue)) { + // fine -- nothing + } else { + log.warn("Detected deprecated key "+deprecatedKey+" with value "+altValue+" used in addition to "+keyProvidingValue+" " + + "with value "+value+" for "+configBag.getDescription()+"; ignoring"); + configBag.remove(deprecatedKey); + } + } else { + log.warn("Detected deprecated key "+deprecatedKey+" with value "+altValue+" used instead of recommended "+preferredKey+"; " + + "promoting to preferred key status; will not be supported in future versions"); + configBag.put(preferredKey, altValue); + configBag.remove(deprecatedKey); + value = altValue; + found = true; + keyProvidingValue = deprecatedKey; + } + } + } + } + + if (found) { + return value; + } else { + return configBag.get(preferredKey); // get the default + } + } + + public static Map<ConfigKey<String>,String> finalAndOriginalSpecs(String finalSpec, Object ...sourcesForOriginalSpec) { + // yuck!: TODO should clean up how these things get passed around + Map<ConfigKey<String>,String> result = MutableMap.of(); + if (finalSpec!=null) + result.put(LocationInternal.FINAL_SPEC, finalSpec); + + String originalSpec = null; + for (Object source: sourcesForOriginalSpec) { + if (source instanceof CharSequence) originalSpec = source.toString(); + else if (source instanceof Map) { + if (originalSpec==null) originalSpec = Strings.toString( ((Map<?,?>)source).get(LocationInternal.ORIGINAL_SPEC) ); + if (originalSpec==null) originalSpec = Strings.toString( ((Map<?,?>)source).get(LocationInternal.ORIGINAL_SPEC.getName()) ); + } + if (originalSpec!=null) break; + } + if (originalSpec==null) originalSpec = finalSpec; + if (originalSpec!=null) + result.put(LocationInternal.ORIGINAL_SPEC, originalSpec); + + return result; + } + + public static boolean isEnabled(ManagementContext mgmt, String prefix) { + ConfigKey<Boolean> key = ConfigKeys.newConfigKeyWithPrefix(prefix+".", LocationConfigKeys.ENABLED); + Boolean enabled = mgmt.getConfig().getConfig(key); + if (enabled!=null) return enabled.booleanValue(); + return true; + } + + +}
