Cleanup node authentication

This commit is contained in:
Ignasi Barrera 2014-12-23 00:17:14 +01:00
parent 4772587722
commit d6d1e7dde5
8 changed files with 105 additions and 234 deletions

View File

@ -6,7 +6,7 @@ Authenticating into the instances:
--------
User:
If no user is provided in GoogleComputeEngineTemplateOptions when launching an instance by default "jclouds" is used.
If no user is provided in GoogleComputeEngineTemplateOptions when launching an instance by default "root" is used.
Credential:

View File

@ -36,6 +36,7 @@ import org.jclouds.compute.ComputeServiceAdapter;
import org.jclouds.compute.domain.Hardware;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.options.TemplateOptions;
import org.jclouds.domain.Location;
import org.jclouds.domain.LocationBuilder;
import org.jclouds.domain.LocationScope;
@ -59,6 +60,7 @@ import org.jclouds.location.suppliers.all.JustProvider;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@ -108,25 +110,17 @@ public final class GoogleComputeEngineServiceAdapter
@Override public NodeAndInitialCredentials<Instance> createNodeWithGroupEncodedIntoName(String group, String name,
Template template) {
checkNotNull(template, "template");
GoogleComputeEngineTemplateOptions options = GoogleComputeEngineTemplateOptions.class.cast(template.getOptions());
checkNotNull(options.network(), "template options must specify a network");
Hardware hardware = checkNotNull(template.getHardware(), "hardware must be set");
checkNotNull(hardware.getUri(), "hardware must have a URI");
checkNotNull(template.getHardware().getUri(), "hardware must have a URI");
checkNotNull(template.getImage().getUri(), "image URI is null");
List<AttachDisk> disks = Lists.newArrayList();
disks.add(AttachDisk.newBootDisk(template.getImage().getUri()));
for (URI existingDisk : options.additionalDisks()) {
disks.add(AttachDisk.existingDisk(existingDisk));
}
NewInstance newInstance = NewInstance.create(
name, // name
hardware.getUri(), // machineType
template.getHardware().getUri(), // machineType
options.network(), // network
disks, // disks
group // description
@ -141,9 +135,11 @@ public final class GoogleComputeEngineServiceAdapter
// Add metadata from template and for ssh key and image id
newInstance.metadata().putAll(options.getUserMetadata());
if (options.getPublicKey() != null) { // TODO: why are we doing this?
newInstance.metadata().put("sshKeys", format("%s:%s %s@localhost", checkNotNull(options.getLoginUser(),
"loginUser cannot be null"), options.getPublicKey(), options.getLoginUser()));
LoginCredentials credentials = resolveNodeCredentials(template);
if (options.getPublicKey() != null) {
newInstance.metadata().put("sshKeys",
format("%s:%s %s@localhost", credentials.getUser(), options.getPublicKey(), credentials.getUser()));
}
String zone = template.getLocation().getId();
@ -174,7 +170,6 @@ public final class GoogleComputeEngineServiceAdapter
// Add lookup for InstanceToNodeMetadata
diskToSourceImage.put(instance.get().disks().get(0).source(), template.getImage().getUri());
LoginCredentials credentials = getFromImageAndOverrideIfRequired(template.getImage(), options);
return new NodeAndInitialCredentials<Instance>(instance.get(), instance.get().selfLink().toString(), credentials);
}
@ -256,40 +251,6 @@ public final class GoogleComputeEngineServiceAdapter
throw new UnsupportedOperationException("suspend is not supported by GCE");
}
// TODO: this entire method is questionable. needs a test case, or to be removed.
private static LoginCredentials getFromImageAndOverrideIfRequired(org.jclouds.compute.domain.Image image,
GoogleComputeEngineTemplateOptions options) {
LoginCredentials defaultCredentials = image.getDefaultCredentials();
String[] keys = defaultCredentials.getPrivateKey().split(":");
String publicKey = keys[0];
String privateKey = keys[1];
LoginCredentials.Builder credentialsBuilder = defaultCredentials.toBuilder();
credentialsBuilder.privateKey(privateKey);
// LoginCredentials from image stores the public key along with the private key in the privateKey field
// @see GoogleComputePopulateDefaultLoginCredentialsForImageStrategy
// so if options doesn't have a public key set we set it from the default
if (options.getPublicKey() == null) {
options.authorizePublicKey(publicKey);
}
if (options.hasLoginPrivateKeyOption()) {
credentialsBuilder.privateKey(options.getPrivateKey());
}
if (options.getLoginUser() != null) {
credentialsBuilder.identity(options.getLoginUser());
}
if (options.hasLoginPasswordOption()) {
credentialsBuilder.password(options.getLoginPassword());
}
if (options.shouldAuthenticateSudo() != null) {
credentialsBuilder.authenticateSudo(options.shouldAuthenticateSudo());
}
LoginCredentials credentials = credentialsBuilder.build();
options.overrideLoginCredentials(credentials);
return credentials;
}
private void waitOperationDone(Operation operation) {
AtomicReference<Operation> operationRef = Atomics.newReference(operation);
@ -306,6 +267,24 @@ public final class GoogleComputeEngineServiceAdapter
}
}
private LoginCredentials resolveNodeCredentials(Template template) {
TemplateOptions options = template.getOptions();
LoginCredentials.Builder credentials = LoginCredentials.builder(template.getImage().getDefaultCredentials());
if (!Strings.isNullOrEmpty(options.getLoginUser())) {
credentials.user(options.getLoginUser());
}
if (!Strings.isNullOrEmpty(options.getLoginPrivateKey())) {
credentials.privateKey(options.getLoginPrivateKey());
}
if (!Strings.isNullOrEmpty(options.getLoginPassword())) {
credentials.password(options.getLoginPassword());
}
if (options.shouldAuthenticateSudo() != null) {
credentials.authenticateSudo(options.shouldAuthenticateSudo());
}
return credentials.build();
}
private static String toName(URI link) {
String path = link.getPath();
return path.substring(path.lastIndexOf('/') + 1);

View File

@ -43,7 +43,6 @@ import org.jclouds.compute.domain.SecurityGroup;
import org.jclouds.compute.extensions.ImageExtension;
import org.jclouds.compute.extensions.SecurityGroupExtension;
import org.jclouds.compute.options.TemplateOptions;
import org.jclouds.compute.strategy.PrioritizeCredentialsFromTemplate;
import org.jclouds.domain.Location;
import org.jclouds.googlecomputeengine.compute.GoogleComputeEngineService;
import org.jclouds.googlecomputeengine.compute.GoogleComputeEngineServiceAdapter;
@ -64,8 +63,6 @@ import org.jclouds.googlecomputeengine.compute.predicates.AllNodesInGroupTermina
import org.jclouds.googlecomputeengine.compute.predicates.AtomicInstanceVisible;
import org.jclouds.googlecomputeengine.compute.predicates.AtomicOperationDone;
import org.jclouds.googlecomputeengine.compute.strategy.CreateNodesWithGroupEncodedIntoNameThenAddToSet;
import org.jclouds.googlecomputeengine.compute.strategy.PopulateDefaultLoginCredentialsForImageStrategy;
import org.jclouds.googlecomputeengine.compute.strategy.UseNodeCredentialsButOverrideFromTemplate;
import org.jclouds.googlecomputeengine.domain.Firewall;
import org.jclouds.googlecomputeengine.domain.Image;
import org.jclouds.googlecomputeengine.domain.Instance;
@ -125,9 +122,6 @@ public final class GoogleComputeEngineServiceContextModule
bind(new TypeLiteral<Function<Network, SecurityGroup>>() {
}).to(NetworkToSecurityGroup.class);
bind(org.jclouds.compute.strategy.PopulateDefaultLoginCredentialsForImageStrategy.class)
.to(PopulateDefaultLoginCredentialsForImageStrategy.class);
bind(org.jclouds.compute.strategy.impl.CreateNodesWithGroupEncodedIntoNameThenAddToSet.class)
.to(CreateNodesWithGroupEncodedIntoNameThenAddToSet.class);
@ -146,10 +140,7 @@ public final class GoogleComputeEngineServiceContextModule
}).to(FindNetworkOrCreate.class);
bind(SecurityGroupExtension.class).to(GoogleComputeEngineSecurityGroupExtension.class);
bind(PrioritizeCredentialsFromTemplate.class).to(UseNodeCredentialsButOverrideFromTemplate.class);
bind(FirewallTagNamingConvention.Factory.class).in(Scopes.SINGLETON);
bindHttpApi(binder(), Resources.class);
}

View File

@ -17,7 +17,6 @@
package org.jclouds.googlecomputeengine.compute.options;
import java.net.URI;
import java.util.List;
import java.util.Map;
import org.jclouds.compute.options.TemplateOptions;
@ -25,13 +24,11 @@ import org.jclouds.domain.LoginCredentials;
import org.jclouds.javax.annotation.Nullable;
import org.jclouds.scriptbuilder.domain.Statement;
import com.google.common.collect.Lists;
/** Instance options specific to Google Compute Engine. */
public final class GoogleComputeEngineTemplateOptions extends TemplateOptions {
private URI network = null;
private final List<URI> additionalDisks = Lists.newArrayList();
private boolean autoCreateKeyPair = true;
@Override
public GoogleComputeEngineTemplateOptions clone() {
@ -46,6 +43,7 @@ public final class GoogleComputeEngineTemplateOptions extends TemplateOptions {
if (to instanceof GoogleComputeEngineTemplateOptions) {
GoogleComputeEngineTemplateOptions eTo = GoogleComputeEngineTemplateOptions.class.cast(to);
eTo.network(network());
eTo.autoCreateKeyPair(autoCreateKeyPair());
}
}
@ -60,10 +58,19 @@ public final class GoogleComputeEngineTemplateOptions extends TemplateOptions {
return network;
}
/** Additional disks to attach to this instance. */
// TODO: test me or remove me!
public List<URI> additionalDisks() {
return additionalDisks;
/**
* Sets whether an SSH key pair should be created automatically.
*/
public GoogleComputeEngineTemplateOptions autoCreateKeyPair(boolean autoCreateKeyPair) {
this.autoCreateKeyPair = autoCreateKeyPair;
return this;
}
/**
* Sets whether an SSH key pair should be created automatically.
*/
public boolean autoCreateKeyPair() {
return autoCreateKeyPair;
}
/**

View File

@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Named;
@ -47,8 +48,11 @@ import org.jclouds.googlecomputeengine.domain.Network;
import org.jclouds.googlecomputeengine.domain.Operation;
import org.jclouds.googlecomputeengine.features.FirewallApi;
import org.jclouds.googlecomputeengine.options.FirewallOptions;
import org.jclouds.logging.Logger;
import org.jclouds.ssh.SshKeyPairGenerator;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
@ -58,7 +62,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
public final class CreateNodesWithGroupEncodedIntoNameThenAddToSet extends
org.jclouds.compute.strategy.impl.CreateNodesWithGroupEncodedIntoNameThenAddToSet {
org.jclouds.compute.strategy.impl.CreateNodesWithGroupEncodedIntoNameThenAddToSet {
public static final String EXTERIOR_RANGE = "0.0.0.0/0";
public static final String DEFAULT_INTERNAL_NETWORK_RANGE = "10.0.0.0/8";
@ -67,67 +71,88 @@ public final class CreateNodesWithGroupEncodedIntoNameThenAddToSet extends
private final LoadingCache<NetworkAndAddressRange, Network> networkMap;
private final Predicate<AtomicReference<Operation>> operationDone;
private final FirewallTagNamingConvention.Factory firewallTagNamingConvention;
private final SshKeyPairGenerator keyGenerator;
@Inject CreateNodesWithGroupEncodedIntoNameThenAddToSet(
CreateNodeWithGroupEncodedIntoName addNodeWithGroupStrategy,
ListNodesStrategy listNodesStrategy,
GroupNamingConvention.Factory namingConvention,
@Named(Constants.PROPERTY_USER_THREADS)
ListeningExecutorService userExecutor,
CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory
customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory,
GoogleComputeEngineApi api,
Predicate<AtomicReference<Operation>> operationDone,
LoadingCache<NetworkAndAddressRange, Network> networkMap,
FirewallTagNamingConvention.Factory firewallTagNamingConvention) {
@Resource
@Named(ComputeServiceConstants.COMPUTE_LOGGER)
protected Logger logger = Logger.NULL;
@Inject
CreateNodesWithGroupEncodedIntoNameThenAddToSet(
CreateNodeWithGroupEncodedIntoName addNodeWithGroupStrategy,
ListNodesStrategy listNodesStrategy,
GroupNamingConvention.Factory namingConvention,
@Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor,
CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory,
GoogleComputeEngineApi api, Predicate<AtomicReference<Operation>> operationDone,
LoadingCache<NetworkAndAddressRange, Network> networkMap,
FirewallTagNamingConvention.Factory firewallTagNamingConvention, SshKeyPairGenerator keyGenerator) {
super(addNodeWithGroupStrategy, listNodesStrategy, namingConvention, userExecutor,
customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory);
customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory);
this.api = api;
this.operationDone = operationDone;
this.networkMap = networkMap;
this.firewallTagNamingConvention = firewallTagNamingConvention;
this.keyGenerator = keyGenerator;
}
@Override // TODO: why synchronized?
public synchronized Map<?, ListenableFuture<Void>> execute(String group, int count,
Template template,
Set<NodeMetadata> goodNodes,
Map<NodeMetadata, Exception> badNodes,
Multimap<NodeMetadata, CustomizationResponse> customizationResponses) {
@Override
public Map<?, ListenableFuture<Void>> execute(String group, int count, Template template,
Set<NodeMetadata> goodNodes, Map<NodeMetadata, Exception> badNodes,
Multimap<NodeMetadata, CustomizationResponse> customizationResponses) {
String sharedResourceName = namingConvention.create().sharedNameForGroup(group);
Template mutableTemplate = template.clone();
GoogleComputeEngineTemplateOptions templateOptions = GoogleComputeEngineTemplateOptions.class.cast(mutableTemplate
.getOptions());
GoogleComputeEngineTemplateOptions templateOptions = GoogleComputeEngineTemplateOptions.class
.cast(mutableTemplate.getOptions());
assert template.getOptions().equals(templateOptions) : "options didn't clone properly";
// get or insert the network and insert a firewall with the users configuration
// get or insert the network and insert a firewall with the users
// configuration
Network network = getOrCreateNetwork(templateOptions, sharedResourceName);
getOrCreateFirewalls(templateOptions, network, firewallTagNamingConvention.get(group));
templateOptions.network(network.selfLink());
templateOptions.userMetadata(ComputeServiceConstants.NODE_GROUP_KEY, group);
// Configure the default credentials, if needed
if (templateOptions.autoCreateKeyPair() && Strings.isNullOrEmpty(templateOptions.getPublicKey())) {
logger.debug(">> creating default keypair...");
Map<String, String> defaultKeys = keyGenerator.get();
templateOptions.authorizePublicKey(defaultKeys.get("public"));
templateOptions.overrideLoginPrivateKey(defaultKeys.get("private"));
}
if (templateOptions.getRunScript() != null && templateOptions.getLoginPrivateKey() == null) {
logger.warn(">> A runScript has been configured but no SSH key has been provided."
+ " Authentication will delegate to the ssh-agent");
}
return super.execute(group, count, mutableTemplate, goodNodes, badNodes, customizationResponses);
}
/**
* Try and find a network either previously created by jclouds or user defined.
* Try and find a network either previously created by jclouds or user
* defined.
*/
private Network getOrCreateNetwork(GoogleComputeEngineTemplateOptions templateOptions, String sharedResourceName) {
String networkName = templateOptions.network() != null ? toName(templateOptions.network()) : sharedResourceName;
return networkMap.apply(NetworkAndAddressRange.create(networkName, DEFAULT_INTERNAL_NETWORK_RANGE, null));
return networkMap.getUnchecked(NetworkAndAddressRange.create(networkName, DEFAULT_INTERNAL_NETWORK_RANGE, null));
}
/**
* Ensures that a firewall exists for every inbound port that the instance requests.
* Ensures that a firewall exists for every inbound port that the instance
* requests.
* <p>
* For each port, there must be a firewall with a name following the {@link FirewallTagNamingConvention},
* with a target tag also following the {@link FirewallTagNamingConvention}, which opens the requested port
* for all sources on both TCP and UDP protocols.
* @see org.jclouds.googlecomputeengine.features.FirewallApi#patch(String, org.jclouds.googlecomputeengine.options.FirewallOptions)
* For each port, there must be a firewall with a name following the
* {@link FirewallTagNamingConvention}, with a target tag also following the
* {@link FirewallTagNamingConvention}, which opens the requested port for
* all sources on both TCP and UDP protocols.
*
* @see org.jclouds.googlecomputeengine.features.FirewallApi#patch(String,
* org.jclouds.googlecomputeengine.options.FirewallOptions)
*/
private void getOrCreateFirewalls(GoogleComputeEngineTemplateOptions templateOptions, Network network,
FirewallTagNamingConvention naming) {
FirewallTagNamingConvention naming) {
FirewallApi firewallApi = api.firewalls();
List<AtomicReference<Operation>> operations = Lists.newArrayList();
@ -138,17 +163,11 @@ public final class CreateNodesWithGroupEncodedIntoNameThenAddToSet extends
if (firewall == null) {
List<String> ports = ImmutableList.of(String.valueOf(port));
List<Rule> rules = ImmutableList.of(Rule.create("tcp", ports), Rule.create("udp", ports));
FirewallOptions firewallOptions = new FirewallOptions()
.name(name)
.network(network.selfLink())
.allowedRules(rules)
.sourceTags(templateOptions.getTags())
.sourceRanges(of(DEFAULT_INTERNAL_NETWORK_RANGE, EXTERIOR_RANGE))
.targetTags(ImmutableList.of(name));
FirewallOptions firewallOptions = new FirewallOptions().name(name).network(network.selfLink())
.allowedRules(rules).sourceTags(templateOptions.getTags())
.sourceRanges(of(DEFAULT_INTERNAL_NETWORK_RANGE, EXTERIOR_RANGE)).targetTags(ImmutableList.of(name));
AtomicReference<Operation> operation = Atomics.newReference(firewallApi.createInNetwork(
firewallOptions.name(),
network.selfLink(),
firewallOptions));
firewallOptions.name(), network.selfLink(), firewallOptions));
operations.add(operation);
}
}

View File

@ -1,69 +0,0 @@
/*
* 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.jclouds.googlecomputeengine.compute.strategy;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.jclouds.compute.config.ComputeServiceProperties.TEMPLATE;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.inject.Named;
import javax.inject.Singleton;
import org.jclouds.compute.domain.TemplateBuilderSpec;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.ssh.internal.RsaSshKeyPairGenerator;
import com.google.inject.Inject;
@Singleton
public class PopulateDefaultLoginCredentialsForImageStrategy implements
org.jclouds.compute.strategy.PopulateDefaultLoginCredentialsForImageStrategy {
private final TemplateBuilderSpec templateBuilder;
private final RsaSshKeyPairGenerator keyPairGenerator;
private String compoundKey;
@Inject
PopulateDefaultLoginCredentialsForImageStrategy(@Named(TEMPLATE) String templateSpec,
RsaSshKeyPairGenerator keyPairGenerator)
throws NoSuchAlgorithmException {
this.templateBuilder = TemplateBuilderSpec.parse(checkNotNull(templateSpec, "template builder spec"));
checkNotNull(templateBuilder.getLoginUser(), "template builder spec must provide a loginUser");
this.keyPairGenerator = checkNotNull(keyPairGenerator, "keypair generator");
}
@PostConstruct
private void generateKeys() {
Map<String, String> keys = keyPairGenerator.get();
// as we need to store both the pubk and the pk, store them separated by : (base64 does not contain that char)
compoundKey = String.format("%s:%s", checkNotNull(keys.get("public"), "public key cannot be null"),
checkNotNull(keys.get("private"), "private key cannot be null"));
}
@Override
public LoginCredentials apply(Object image) {
return LoginCredentials.builder()
.authenticateSudo(templateBuilder.getAuthenticateSudo() != null ?
templateBuilder.getAuthenticateSudo() : false)
.privateKey(compoundKey)
.user(templateBuilder.getLoginUser()).build();
}
}

View File

@ -1,57 +0,0 @@
/*
* 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.jclouds.googlecomputeengine.compute.strategy;
import static com.google.common.base.Preconditions.checkNotNull;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.options.RunScriptOptions;
import org.jclouds.compute.strategy.PrioritizeCredentialsFromTemplate;
import org.jclouds.domain.LoginCredentials;
import com.google.common.base.Function;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
* GCE needs the credentials to insert the node so the node credentials already take the Image credentials into account,
* as such only overriding the TemplateOptions credentials is required.
*/
@Singleton
public class UseNodeCredentialsButOverrideFromTemplate extends PrioritizeCredentialsFromTemplate {
@Inject
public UseNodeCredentialsButOverrideFromTemplate(
Function<Template, LoginCredentials> credentialsFromImageOrTemplateOptions) {
super(credentialsFromImageOrTemplateOptions);
}
public LoginCredentials apply(Template template, LoginCredentials fromNode) {
RunScriptOptions options = checkNotNull(template.getOptions(), "template options are required");
LoginCredentials.Builder builder = LoginCredentials.builder(fromNode);
if (options.getLoginUser() != null)
builder.user(template.getOptions().getLoginUser());
if (options.getLoginPassword() != null)
builder.password(options.getLoginPassword());
if (options.getLoginPrivateKey() != null)
builder.privateKey(options.getLoginPrivateKey());
if (options.shouldAuthenticateSudo() != null && options.shouldAuthenticateSudo())
builder.authenticateSudo(true);
return builder.build();
}
}

View File

@ -166,7 +166,8 @@ public class GoogleComputeEngineServiceMockTest extends BaseGoogleComputeEngineA
ComputeService computeService = computeService();
GoogleComputeEngineTemplateOptions options = computeService.templateOptions()
.as(GoogleComputeEngineTemplateOptions.class).tags(ImmutableSet.of("aTag")).blockUntilRunning(false);
.as(GoogleComputeEngineTemplateOptions.class).autoCreateKeyPair(false)
.tags(ImmutableSet.of("aTag")).blockUntilRunning(false);
Template template = computeService.templateBuilder().options(options).build();
NodeMetadata node = getOnlyElement(computeService.createNodesInGroup("test", 1, template));