/*
* 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.jclouds;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.NoSuchElementException;
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.location.BasicLocationRegistry;
import org.apache.brooklyn.core.location.LocationConfigKeys;
import org.apache.brooklyn.core.location.LocationConfigUtils;
import org.apache.brooklyn.core.location.internal.LocationInternal;
import org.jclouds.apis.ApiMetadata;
import org.jclouds.apis.Apis;
import org.jclouds.providers.ProviderMetadata;
import org.jclouds.providers.Providers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.util.text.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
@SuppressWarnings("rawtypes")
public class JcloudsLocationResolver implements LocationResolver {
public static final Logger log = LoggerFactory.getLogger(JcloudsLocationResolver.class);
private static final String JCLOUDS = "jclouds";
public static final Map<String,ProviderMetadata> PROVIDERS = getProvidersMap();
public static final Map<String,ApiMetadata> APIS = getApisMap();
private static Map<String,ProviderMetadata> getProvidersMap() {
Map<String,ProviderMetadata> result = Maps.newLinkedHashMap();
for (ProviderMetadata p: Providers.all()) {
result.put(p.getId(), p);
}
return ImmutableMap.copyOf(result);
}
private static Map<String,ApiMetadata> getApisMap() {
Map<String,ApiMetadata> result = Maps.newLinkedHashMap();
for (ApiMetadata api: Apis.all()) {
result.put(api.getId(), api);
}
return ImmutableMap.copyOf(result);
}
public static final Collection<String> AWS_REGIONS = Arrays.asList(
// from http://docs.amazonwebservices.com/general/latest/gr/rande.html as of Apr 2012.
// it is suggested not to maintain this list here, instead to require aws-ec2 explicitly named.
"eu-west-1","us-east-1","us-west-1","us-west-2","ap-southeast-1","ap-northeast-1","sa-east-1");
private ManagementContext managementContext;
@Override
public void init(ManagementContext managementContext) {
this.managementContext = checkNotNull(managementContext, "managementContext");
}
protected class JcloudsSpecParser {
String providerOrApi;
String parameter;
public JcloudsSpecParser parse(String spec, boolean dryrun) {
JcloudsSpecParser result = new JcloudsSpecParser();
int split = spec.indexOf(':');
if (split<0) {
if (spec.equalsIgnoreCase(getPrefix())) {
if (dryrun) return null;
throw new IllegalArgumentException("Cannot use '"+spec+"' as a location ID; it is insufficient. "+
"Try jclouds:aws-ec2 (for example).");
}
result.providerOrApi = spec;
result.parameter = null;
} else {
result.providerOrApi = spec.substring(0, split);
result.parameter = spec.substring(split+1);
int numJcloudsPrefixes = 0;
while (result.providerOrApi.equalsIgnoreCase(getPrefix())) {
//strip any number of jclouds: prefixes, for use by static "resolve" method
numJcloudsPrefixes++;
result.providerOrApi = result.parameter;
result.parameter = null;
split = result.providerOrApi.indexOf(':');
if (split>=0) {
result.parameter = result.providerOrApi.substring(split+1);
result.providerOrApi = result.providerOrApi.substring(0, split);
}
}
if (!dryrun && numJcloudsPrefixes > 1) {
log.warn("Use of deprecated location spec '"+spec+"'; in future use a single \"jclouds\" prefix");
}
}
if (result.parameter==null && AWS_REGIONS.contains(result.providerOrApi)) {
// treat amazon as a default
result.parameter = result.providerOrApi;
result.providerOrApi = "aws-ec2";
if (!dryrun)
log.warn("Use of deprecated location '"+result.parameter+"'; in future refer to with explicit " +
"provider '"+result.providerOrApi+":"+result.parameter+"'");
}
return result;
}
public boolean isProvider() {
return PROVIDERS.containsKey(providerOrApi);
}
public boolean isApi() {
return APIS.containsKey(providerOrApi);
}
public String getProviderOrApi() {
return providerOrApi;
}
public String getParameter() {
return parameter;
}
}
@Override
@SuppressWarnings("unchecked")
public JcloudsLocation newLocationFromString(Map locationFlags, String spec, LocationRegistry registry) {
Map globalProperties = registry.getProperties();
JcloudsSpecParser details = new JcloudsSpecParser().parse(spec, false);
String namedLocation = (String) locationFlags.get(LocationInternal.NAMED_SPEC_NAME.getName());
boolean isProvider = details.isProvider();
String providerOrApi = details.providerOrApi;
// gce claims to be an api ... perhaps just a bug? email sent to jclouds dev list, 28 mar 2014
isProvider = isProvider || "google-compute-engine".equals(providerOrApi);
if (Strings.isEmpty(providerOrApi)) {
throw new IllegalArgumentException("Cloud provider/API type not specified in spec \""+spec+"\"");
}
if (!isProvider && !details.isApi()) {
throw new NoSuchElementException("Cloud provider/API type "+providerOrApi+" is not supported by jclouds");
}
// For everything in brooklyn.properties, only use things with correct prefix (and remove that prefix).
// But for everything passed in via locationFlags, pass those as-is.
// TODO Should revisit the locationFlags: where are these actually used? Reason accepting properties without
// full prefix is that the map's context is explicitly this location, rather than being generic properties.
Map allProperties = getAllProperties(registry, globalProperties);
String regionOrEndpoint = details.parameter;
if (regionOrEndpoint==null && isProvider) regionOrEndpoint = (String)locationFlags.get(LocationConfigKeys.CLOUD_REGION_ID.getName());
Map jcloudsProperties = new JcloudsPropertiesFromBrooklynProperties().getJcloudsProperties(providerOrApi, regionOrEndpoint, namedLocation, allProperties);
jcloudsProperties.putAll(locationFlags);
if (regionOrEndpoint!=null) {
// apply the regionOrEndpoint (e.g. from the parameter) as appropriate -- but only if it has not been overridden
if (isProvider) {
// providers from ServiceLoader take a location (endpoint already configured), and optionally a region name
// NB blank might be supplied if spec string is "mycloud:" -- that should be respected,
// whereas no parameter/regionName ie null value -- "mycloud" -- means don't set
if (Strings.isBlank(Strings.toString(jcloudsProperties.get(JcloudsLocationConfig.CLOUD_REGION_ID.getName()))))
jcloudsProperties.put(JcloudsLocationConfig.CLOUD_REGION_ID.getName(), regionOrEndpoint);
} else {
// other "providers" are APIs so take an _endpoint_ (but not a location);
// see note above re null here
if (Strings.isBlank(Strings.toString(jcloudsProperties.get(JcloudsLocationConfig.CLOUD_ENDPOINT.getName()))))
jcloudsProperties.put(JcloudsLocationConfig.CLOUD_ENDPOINT.getName(), regionOrEndpoint);
}
}
return managementContext.getLocationManager().createLocation(LocationSpec.create(getLocationClass())
.configure(LocationConfigUtils.finalAndOriginalSpecs(spec, jcloudsProperties, globalProperties, namedLocation))
.configure(jcloudsProperties) );
}
@SuppressWarnings("unchecked")
private Map getAllProperties(LocationRegistry registry, Map<?,?> properties) {
Map<Object,Object> allProperties = Maps.newHashMap();
if (registry!=null) allProperties.putAll(registry.getProperties());
allProperties.putAll(properties);
return allProperties;
}
@Override
public String getPrefix() {
return JCLOUDS;
}
protected Class<? extends JcloudsLocation> getLocationClass() {
return JcloudsLocation.class;
}
@Override
public boolean accepts(String spec, LocationRegistry registry) {
if (BasicLocationRegistry.isResolverPrefixForSpec(this, spec, true)) return true;
JcloudsSpecParser details = new JcloudsSpecParser().parse(spec, true);
if (details==null) return false;
if (details.isProvider() || details.isApi()) return true;
return false;
}
}