From 22d127b283c0e72336f8d29ed244d238e482d4ac Mon Sep 17 00:00:00 2001 From: Richard Downer Date: Wed, 21 Jan 2015 12:41:22 +0000 Subject: [PATCH] JcloudsLocation set arbitrary template options Adds new config key for JcloudsLocation, templateOptions, that takes a Map for setting arbitrary template options. For each entry, searches for a method with the same name as the key, with a single parameter that TypeCoercions supports. The method is then invoked with the value. If such a method cannot be found, then a warning is printed to the log. The config key's value can be expressed as a YAML fragment in blueprints or in brooklyn.properties: ```properties brooklyn.location.named.softlayer-ams01.templateOptions={ domainName: 'example.com' } ``` ```yaml location: jclouds:softlayer:ams01: templateOptions: domainName: 'example.com' ``` --- .../location/jclouds/JcloudsLocation.java | 45 ++++++++- .../jclouds/JcloudsLocationConfig.java | 4 + ...ionTemplateOptionsCustomisersLiveTest.java | 96 +++++++++++++++++++ 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 locations/jclouds/src/test/java/brooklyn/location/jclouds/JcloudsLocationTemplateOptionsCustomisersLiveTest.java diff --git a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java index ccf5e17e8d..054ce93741 100644 --- a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java +++ b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java @@ -30,6 +30,9 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.security.KeyPair; import java.util.ArrayList; import java.util.Arrays; @@ -48,6 +51,7 @@ import javax.annotation.Nullable; +import com.google.common.reflect.TypeToken; import org.jclouds.abiquo.compute.options.AbiquoTemplateOptions; import org.jclouds.cloudstack.compute.options.CloudStackTemplateOptions; import org.jclouds.compute.ComputeService; @@ -831,7 +835,7 @@ private static interface CustomizeTemplateBuilder { void apply(TemplateBuilder tb, ConfigBag props, Object v); } - private static interface CustomizeTemplateOptions { + public static interface CustomizeTemplateOptions { void apply(TemplateOptions tb, ConfigBag props, Object v); } @@ -1050,6 +1054,43 @@ public void apply(TemplateOptions t, ConfigBag props, Object v) { public void apply(TemplateOptions t, ConfigBag props, Object v) { t.networks((String)v); }}) + .put(TEMPLATE_OPTIONS, new CustomizeTemplateOptions() { + @Override + public void apply(TemplateOptions options, ConfigBag config, Object v) { + if (v == null) return; + @SuppressWarnings("unchecked") Map optionsMap = (Map) v; + if (optionsMap.isEmpty()) return; + + Class clazz = options.getClass(); + Iterable methods = Arrays.asList(clazz.getMethods()); + for(final Map.Entry option : optionsMap.entrySet()) { + Optional methodOptional = Iterables.tryFind(methods, new Predicate() { + @Override + public boolean apply(@Nullable Method input) { + // Matches a method with the expected name, and a single parameter that TypeCoercions + // can coerce to + if (input == null) return false; + if (!input.getName().equals(option.getKey())) return false; + Type[] parameterTypes = input.getGenericParameterTypes(); + return parameterTypes.length == 1 + && TypeCoercions.tryCoerce(option.getValue(), TypeToken.of(parameterTypes[0])).isPresentAndNonNull(); + } + }); + if(methodOptional.isPresent()) { + try { + Method method = methodOptional.get(); + method.invoke(options, TypeCoercions.coerce(option.getValue(), TypeToken.of(method.getGenericParameterTypes()[0]))); + } catch (IllegalAccessException e) { + throw Exceptions.propagate(e); + } catch (InvocationTargetException e) { + throw Exceptions.propagate(e); + } + } else { + LOG.warn("Ignoring request to set template option {} because this is not supported by {}", new Object[] { option.getKey(), clazz.getCanonicalName() }); + } + } + } + }) .build(); private static boolean listedAvailableTemplatesOnNoSuchTemplate = false; @@ -1140,7 +1181,7 @@ public Template buildTemplate(ComputeService computeService, ConfigBag config) { if (config.containsKey(key)) code.apply(options, config, config.get(key)); } - + return template; } diff --git a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocationConfig.java b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocationConfig.java index 5047166684..b7b61a760a 100644 --- a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocationConfig.java +++ b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocationConfig.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.concurrent.Semaphore; import org.jclouds.Constants; @@ -249,6 +250,9 @@ public interface JcloudsLocationConfig extends CloudLocationConfig { "Registry/Factory for creating jclouds ComputeService; default is almost always fine, except where tests want to customize behaviour", ComputeServiceRegistryImpl.INSTANCE); + public static final ConfigKey> TEMPLATE_OPTIONS = ConfigKeys.newConfigKey( + new TypeToken>() {}, "templateOptions", "Additional jclouds template options"); + // TODO // "noDefaultSshKeys" - hints that local ssh keys should not be read as defaults diff --git a/locations/jclouds/src/test/java/brooklyn/location/jclouds/JcloudsLocationTemplateOptionsCustomisersLiveTest.java b/locations/jclouds/src/test/java/brooklyn/location/jclouds/JcloudsLocationTemplateOptionsCustomisersLiveTest.java new file mode 100644 index 0000000000..f4c44a4af1 --- /dev/null +++ b/locations/jclouds/src/test/java/brooklyn/location/jclouds/JcloudsLocationTemplateOptionsCustomisersLiveTest.java @@ -0,0 +1,96 @@ +/* + * 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 brooklyn.location.jclouds; + +import brooklyn.config.ConfigKey; +import brooklyn.location.NoMachinesAvailableException; +import brooklyn.location.basic.SshMachineLocation; +import brooklyn.util.collections.MutableMap; +import brooklyn.util.config.ConfigBag; +import brooklyn.util.ssh.BashCommands; +import brooklyn.util.text.Identifiers; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions; +import org.jclouds.compute.options.TemplateOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static org.testng.Assert.assertEquals; + +public class JcloudsLocationTemplateOptionsCustomisersLiveTest extends AbstractJcloudsLiveTest { + + private static final String LOCATION_SPEC = AWS_EC2_PROVIDER + ":" + AWS_EC2_USEAST_REGION_NAME; + + @BeforeMethod(alwaysRun=true) + @Override + public void setUp() throws Exception { + super.setUp(); + jcloudsLocation = resolve(LOCATION_SPEC); + } + + // Doesn't actually do much with the cloud, but jclouds requires identity and credential before it will work + @Test(groups = "Live") + public void testGeneralPurposeTemplateOptionCustomisation() throws Exception { + ConfigKey> key = JcloudsLocationConfig.TEMPLATE_OPTIONS; + + ConfigBag config = ConfigBag.newInstance() + .configure(key, ImmutableMap.of("iamInstanceProfileName", "helloworld")); + AWSEC2TemplateOptions templateOptions = jcloudsLocation.getComputeService().templateOptions().as(AWSEC2TemplateOptions.class); + + invokeCustomizeTemplateOptions(templateOptions, JcloudsLocationConfig.TEMPLATE_OPTIONS, config); + + assertEquals(templateOptions.getIAMInstanceProfileName(), "helloworld"); + } + + /** + * Invoke a specific template options customizer on a TemplateOptions instance. + * + * @param templateOptions the TemplateOptions instance that you expect the customizer to modify. + * @param keyToTest the config key that identifies the customizer. This must be present in both @{code locationConfig} and @{link JcloudsLocation.SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES}. + * @param locationConfig simulated configuration for the location. This must contain at least an entry for @{code keyToTest}. + */ + private void invokeCustomizeTemplateOptions(TemplateOptions templateOptions, ConfigKey keyToTest, ConfigBag locationConfig) { + checkNotNull(templateOptions, "templateOptions"); + checkNotNull(keyToTest, "keyToTest"); + checkNotNull(locationConfig, "locationConfig"); + checkState(JcloudsLocation.SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES.containsKey(keyToTest), + "SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES does not contain a customiser for the key " + keyToTest.getName()); + checkState(locationConfig.containsKey(keyToTest), + "location config does not contain the key " + keyToTest.getName()); + + JcloudsLocation.CustomizeTemplateOptions code = JcloudsLocation.SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES.get(keyToTest); + code.apply(templateOptions, locationConfig, locationConfig.get(keyToTest)); + } + + private JcloudsLocation resolve(String spec) { + return (JcloudsLocation) managementContext.getLocationRegistry().resolve("jclouds:"+spec); + } +}