JCLOUDS-607: ComputeService.createNodesInGroup throws NPE on FloatingIPApi.create()

This commit is contained in:
Christopher Dancy 2014-06-30 16:01:43 -04:00 committed by Andrew Phillips
parent 4cf6efe39a
commit 3659a5f583
6 changed files with 245 additions and 36 deletions

View File

@ -19,8 +19,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_RUNNING;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Resource;
@ -32,6 +33,7 @@ import org.jclouds.compute.domain.NodeMetadataBuilder;
import org.jclouds.compute.reference.ComputeServiceConstants;
import org.jclouds.logging.Logger;
import org.jclouds.openstack.nova.v2_0.NovaApi;
import org.jclouds.openstack.nova.v2_0.compute.options.NodeAndNovaTemplateOptions;
import org.jclouds.openstack.nova.v2_0.domain.FloatingIP;
import org.jclouds.openstack.nova.v2_0.domain.zonescoped.ZoneAndId;
import org.jclouds.openstack.nova.v2_0.extensions.FloatingIPApi;
@ -39,6 +41,7 @@ import org.jclouds.rest.InsufficientResourcesException;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
@ -49,7 +52,7 @@ import com.google.common.collect.Lists;
* A function for adding and allocating an ip to a node
*/
public class AllocateAndAddFloatingIpToNode implements
Function<AtomicReference<NodeMetadata>, AtomicReference<NodeMetadata>> {
Function<AtomicReference<NodeAndNovaTemplateOptions>, AtomicReference<NodeMetadata>> {
@Resource
@Named(ComputeServiceConstants.COMPUTE_LOGGER)
@ -68,39 +71,77 @@ public class AllocateAndAddFloatingIpToNode implements
}
@Override
public AtomicReference<NodeMetadata> apply(AtomicReference<NodeMetadata> input) {
checkState(nodeRunning.apply(input), "node never achieved state running %s", input.get());
NodeMetadata node = input.get();
public AtomicReference<NodeMetadata> apply(AtomicReference<NodeAndNovaTemplateOptions> input) {
checkState(nodeRunning.apply(input.get().getNodeMetadata()), "node never achieved state running %s", input.get().getNodeMetadata());
NodeMetadata node = input.get().getNodeMetadata().get();
// node's location is a host
String zoneId = node.getLocation().getParent().getId();
FloatingIPApi floatingIpApi = novaApi.getFloatingIPExtensionForZone(zoneId).get();
Optional<Set<String>> poolNames = input.get().getNovaTemplateOptions().get().getFloatingIpPoolNames();
Optional<FloatingIP> ip = allocateFloatingIPForNode(floatingIpApi, poolNames, node.getId());
if (!ip.isPresent()) {
throw new InsufficientResourcesException("Failed to allocate a FloatingIP for node(" + node.getId() + ")");
}
logger.debug(">> adding floatingIp(%s) to node(%s)", ip.get().getIp(), node.getId());
floatingIpApi.addToServer(ip.get().getIp(), node.getProviderId());
input.get().getNodeMetadata().set(NodeMetadataBuilder.fromNodeMetadata(node).publicAddresses(ImmutableSet.of(ip.get().getIp())).build());
floatingIpCache.invalidate(ZoneAndId.fromSlashEncoded(node.getId()));
return input.get().getNodeMetadata();
}
/**
* Allocates a FloatingIP for a given Node
*
* @param floatingIpApi FloatingIPApi to create or query for a valid FloatingIP
* @param poolNames optional set of pool names from which we will attempt to allocate an IP from. Most cases this is null
* @param nodeID optional id of the Node we are trying to allocate a FloatingIP for. Used here only for logging purposes
* @return Optional<FloatingIP>
*/
private Optional<FloatingIP> allocateFloatingIPForNode(FloatingIPApi floatingIpApi, Optional<Set<String>> poolNames, String nodeID) {
FloatingIP ip = null;
try {
logger.debug(">> allocating or reassigning floating ip for node(%s)", node.getId());
ip = floatingIpApi.create();
} catch (InsufficientResourcesException e) {
logger.trace("<< [%s] allocating a new floating ip for node(%s)", e.getMessage(), node.getId());
logger.trace(">> searching for existing, unassigned floating ip for node(%s)", node.getId());
ArrayList<FloatingIP> unassignedIps = Lists.newArrayList(Iterables.filter(floatingIpApi.list(),
new Predicate<FloatingIP>() {
@Override
public boolean apply(FloatingIP arg0) {
return arg0.getFixedIp() == null;
}
}));
// try to prevent multiple parallel launches from choosing the same ip.
Collections.shuffle(unassignedIps);
ip = Iterables.getLast(unassignedIps);
// 1.) Attempt to allocate from optionally passed poolNames
if (poolNames.isPresent()) {
for (String poolName : poolNames.get()) {
try {
logger.debug(">> allocating floating IP from pool %s for node(%s)", poolName, nodeID);
ip = floatingIpApi.allocateFromPool(poolName);
if (ip != null)
return Optional.of(ip);
} catch (InsufficientResourcesException ire){
logger.trace("<< [%s] failed to allocate floating IP from pool %s for node(%s)", ire.getMessage(), poolName, nodeID);
}
}
}
logger.debug(">> adding floatingIp(%s) to node(%s)", ip.getIp(), node.getId());
floatingIpApi.addToServer(ip.getIp(), node.getProviderId());
input.set(NodeMetadataBuilder.fromNodeMetadata(node).publicAddresses(ImmutableSet.of(ip.getIp())).build());
floatingIpCache.invalidate(ZoneAndId.fromSlashEncoded(node.getId()));
return input;
// 2.) Attempt to allocate, if necessary, via 'create()' call
try {
logger.debug(">> creating floating IP for node(%s)", nodeID);
ip = floatingIpApi.create();
if (ip != null)
return Optional.of(ip);
} catch (InsufficientResourcesException ire) {
logger.trace("<< [%s] failed to create floating IP for node(%s)", ire.getMessage(), nodeID);
}
// 3.) If no IP was found make final attempt by searching through list of available IP's
logger.trace(">> searching for existing, unassigned floating IP for node(%s)", nodeID);
List<FloatingIP> unassignedIps = Lists.newArrayList(Iterables.filter(floatingIpApi.list(),
new Predicate<FloatingIP>() {
@Override
public boolean apply(FloatingIP arg0) {
return arg0.getFixedIp() == null;
}
}));
// try to prevent multiple parallel launches from choosing the same ip.
Collections.shuffle(unassignedIps);
ip = Iterables.getLast(unassignedIps);
return Optional.fromNullable(ip);
}
@Override

View File

@ -0,0 +1,54 @@
/*
* 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.openstack.nova.v2_0.compute.options;
import java.util.concurrent.atomic.AtomicReference;
import org.jclouds.compute.domain.NodeMetadata;
import com.google.common.util.concurrent.Atomics;
/**
* Simple data-structure for holding a NodeMetadata object along with a
* corresponding NovaTemplateOptions object.
*/
public class NodeAndNovaTemplateOptions {
private final AtomicReference<NodeMetadata> nodeMetadata;
private final AtomicReference<NovaTemplateOptions> novaTemplateOptions;
protected NodeAndNovaTemplateOptions(AtomicReference<NodeMetadata> nodeMetadata, AtomicReference<NovaTemplateOptions> novaTemplateOptions) {
this.nodeMetadata = nodeMetadata;
this.novaTemplateOptions = novaTemplateOptions;
}
public AtomicReference<NodeMetadata> getNodeMetadata() {
return nodeMetadata;
}
public AtomicReference<NovaTemplateOptions> getNovaTemplateOptions() {
return novaTemplateOptions;
}
public static NodeAndNovaTemplateOptions newReference(AtomicReference<NodeMetadata> node, AtomicReference<NovaTemplateOptions> options) {
return new NodeAndNovaTemplateOptions(node, options);
}
public static AtomicReference<NodeAndNovaTemplateOptions> newAtomicReference(AtomicReference<NodeMetadata> node, AtomicReference<NovaTemplateOptions> options) {
return Atomics.newReference(NodeAndNovaTemplateOptions.newReference(node, options));
}
}

View File

@ -63,6 +63,8 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
if (to instanceof NovaTemplateOptions) {
NovaTemplateOptions eTo = NovaTemplateOptions.class.cast(to);
eTo.autoAssignFloatingIp(shouldAutoAssignFloatingIp());
if (getFloatingIpPoolNames().isPresent())
eTo.floatingIpPoolNames(getFloatingIpPoolNames().get());
if (getSecurityGroupNames().isPresent())
eTo.securityGroupNames(getSecurityGroupNames().get());
eTo.generateKeyPair(shouldGenerateKeyPair());
@ -80,6 +82,7 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
}
protected boolean autoAssignFloatingIp = false;
protected Optional<Set<String>> floatingIpPoolNames = Optional.absent();
protected Optional<Set<String>> securityGroupNames = Optional.absent();
protected boolean generateKeyPair = false;
protected String keyPairName;
@ -96,6 +99,7 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
return false;
NovaTemplateOptions that = NovaTemplateOptions.class.cast(o);
return super.equals(that) && equal(this.autoAssignFloatingIp, that.autoAssignFloatingIp)
&& equal(this.floatingIpPoolNames, that.floatingIpPoolNames)
&& equal(this.securityGroupNames, that.securityGroupNames)
&& equal(this.generateKeyPair, that.generateKeyPair)
&& equal(this.keyPairName, that.keyPairName)
@ -107,7 +111,7 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
@Override
public int hashCode() {
return Objects.hashCode(super.hashCode(), autoAssignFloatingIp, securityGroupNames, generateKeyPair, keyPairName, userData, diskConfig, configDrive, novaNetworks);
return Objects.hashCode(super.hashCode(), autoAssignFloatingIp, floatingIpPoolNames, securityGroupNames, generateKeyPair, keyPairName, userData, diskConfig, configDrive, novaNetworks);
}
@Override
@ -115,6 +119,8 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
ToStringHelper toString = super.string();
if (!autoAssignFloatingIp)
toString.add("autoAssignFloatingIp", autoAssignFloatingIp);
if (floatingIpPoolNames.isPresent())
toString.add("floatingIpPoolNames", floatingIpPoolNames.get());
if (securityGroupNames.isPresent())
toString.add("securityGroupNames", securityGroupNames.get());
if (generateKeyPair)
@ -130,13 +136,30 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
public static final NovaTemplateOptions NONE = new NovaTemplateOptions();
/**
* @see #shouldAutoAssignFloatingIp()
* @see #getFloatingIpPoolNames()
*/
public NovaTemplateOptions autoAssignFloatingIp(boolean enable) {
this.autoAssignFloatingIp = enable;
return this;
}
/**
* @see #getFloatingIpPoolNames()
*/
public NovaTemplateOptions floatingIpPoolNames(String... floatingIpPoolNames) {
return floatingIpPoolNames(ImmutableSet.copyOf(checkNotNull(floatingIpPoolNames, "floatingIpPoolNames")));
}
/**
* @see #getFloatingIpPoolNames()
*/
public NovaTemplateOptions floatingIpPoolNames(Iterable<String> floatingIpPoolNames) {
for (String groupName : checkNotNull(floatingIpPoolNames, "floatingIpPoolNames"))
checkNotNull(emptyToNull(groupName), "all floating-ip-pool-names must be non-empty");
this.floatingIpPoolNames = Optional.<Set<String>> of(ImmutableSet.copyOf(floatingIpPoolNames));
return this;
}
/**
* @see #shouldGenerateKeyPair()
*/
@ -183,6 +206,18 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
return autoAssignFloatingIp;
}
/**
* The floating IP pool name(s) to use when allocating a FloatingIP. Applicable
* only if #shouldAutoAssignFloatingIp() returns true. If not set will attempt to
* use whatever FloatingIP(s) can be found regardless of which pool they originated
* from
*
* @return floating-ip-pool names to use
*/
public Optional<Set<String>> getFloatingIpPoolNames() {
return floatingIpPoolNames;
}
/**
* Specifies the keypair used to run instances with
* @return the keypair to be used
@ -247,6 +282,22 @@ public class NovaTemplateOptions extends TemplateOptions implements Cloneable {
return new NovaTemplateOptions().autoAssignFloatingIp(enable);
}
/**
* @see #getFloatingIpPoolNames()
*/
public NovaTemplateOptions floatingIpPoolNames(String... floatingIpPoolNames) {
NovaTemplateOptions options = new NovaTemplateOptions();
return NovaTemplateOptions.class.cast(options.floatingIpPoolNames(floatingIpPoolNames));
}
/**
* @see #getFloatingIpPoolNames()
*/
public NovaTemplateOptions floatingIpPoolNames(Iterable<String> floatingIpPoolNames) {
NovaTemplateOptions options = new NovaTemplateOptions();
return NovaTemplateOptions.class.cast(options.floatingIpPoolNames(floatingIpPoolNames));
}
/**
* @see NovaTemplateOptions#shouldGenerateKeyPair()
*/

View File

@ -42,16 +42,19 @@ import org.jclouds.compute.strategy.ListNodesStrategy;
import org.jclouds.compute.strategy.impl.CreateNodesWithGroupEncodedIntoNameThenAddToSet;
import org.jclouds.openstack.nova.v2_0.NovaApi;
import org.jclouds.openstack.nova.v2_0.compute.functions.AllocateAndAddFloatingIpToNode;
import org.jclouds.openstack.nova.v2_0.compute.options.NodeAndNovaTemplateOptions;
import org.jclouds.openstack.nova.v2_0.compute.options.NovaTemplateOptions;
import org.jclouds.openstack.nova.v2_0.domain.KeyPair;
import org.jclouds.openstack.nova.v2_0.domain.zonescoped.SecurityGroupInZone;
import org.jclouds.openstack.nova.v2_0.domain.zonescoped.ZoneAndName;
import org.jclouds.openstack.nova.v2_0.domain.zonescoped.ZoneSecurityGroupNameAndPorts;
import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Multimap;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.Atomics;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
@ -149,13 +152,21 @@ public class ApplyNovaTemplateOptionsCreateNodesWithGroupEncodedIntoNameThenAddT
final String name, Template template) {
ListenableFuture<AtomicReference<NodeMetadata>> future = super.createNodeInGroupWithNameAndTemplate(group, name, template);
NovaTemplateOptions templateOptions = NovaTemplateOptions.class.cast(template.getOptions());
final NovaTemplateOptions templateOptions = NovaTemplateOptions.class.cast(template.getOptions());
if (templateOptions.shouldAutoAssignFloatingIp()) {
return Futures.transform(future, createAndAddFloatingIpToNode, userExecutor);
ListenableFuture<AtomicReference<NodeAndNovaTemplateOptions>> nodeAndNovaTemplateOptions = Futures.transform(future,
new Function<AtomicReference<NodeMetadata>, AtomicReference<NodeAndNovaTemplateOptions>>() {
@Override
public AtomicReference<NodeAndNovaTemplateOptions> apply(AtomicReference<NodeMetadata> input) {
return NodeAndNovaTemplateOptions.newAtomicReference(input, Atomics.newReference(templateOptions));
}
}
);
return Futures.transform(nodeAndNovaTemplateOptions, createAndAddFloatingIpToNode, userExecutor);
} else {
return future;
}
}
}

View File

@ -31,6 +31,8 @@ import org.jclouds.domain.LocationScope;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpResponse;
import org.jclouds.openstack.nova.v2_0.compute.options.NodeAndNovaTemplateOptions;
import org.jclouds.openstack.nova.v2_0.compute.options.NovaTemplateOptions;
import org.jclouds.openstack.nova.v2_0.internal.BaseNovaComputeServiceExpectTest;
import org.testng.annotations.Test;
@ -53,6 +55,7 @@ public class AllocateAndAddFloatingIpToNodeExpectTest extends BaseNovaComputeSer
final NodeMetadata node = new NodeMetadataBuilder().id("az-1.region-a.geo-1/71592").providerId("71592").location(
host).name("Server 71592").status(Status.RUNNING).privateAddresses(ImmutableSet.of("10.4.27.237"))
.credentials(LoginCredentials.builder().password("foo").build()).build();
final NovaTemplateOptions options = NovaTemplateOptions.Builder.autoAssignFloatingIp(false);
HttpRequest createFloatingIP = HttpRequest.builder().method("POST").endpoint(
URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-floating-ips")).headers(
@ -75,9 +78,13 @@ public class AllocateAndAddFloatingIpToNodeExpectTest extends BaseNovaComputeSer
.getInstance(AllocateAndAddFloatingIpToNode.class);
AtomicReference<NodeMetadata> nodeRef = Atomics.newReference(node);
fn.apply(nodeRef);
AtomicReference<NovaTemplateOptions> optionsRef = Atomics.newReference(options);
AtomicReference<NodeAndNovaTemplateOptions> nodeNovaRef = NodeAndNovaTemplateOptions.newAtomicReference(nodeRef, optionsRef);
fn.apply(nodeNovaRef);
NodeMetadata node1 = nodeRef.get();
assertNotNull(node1);
assertNotNull(optionsRef.get());
assertEquals(node1.getPublicAddresses(), ImmutableSet.of("10.0.0.3"));
assertEquals(node1.getCredentials(), node.getCredentials());
@ -94,7 +101,7 @@ public class AllocateAndAddFloatingIpToNodeExpectTest extends BaseNovaComputeSer
return addFloatingIPRequest;
}
public void testAllocateWhenAllocationFailsLookupUnusedIpAddToServerAndUpdatesNodeMetadata() throws Exception {
public void testAllocateWhenAllocationFailsOn400LookupUnusedIpAddToServerAndUpdatesNodeMetadata() throws Exception {
HttpResponse createFloatingIPResponse = HttpResponse
.builder()
.statusCode(400)
@ -122,10 +129,51 @@ public class AllocateAndAddFloatingIpToNodeExpectTest extends BaseNovaComputeSer
.getInstance(AllocateAndAddFloatingIpToNode.class);
AtomicReference<NodeMetadata> nodeRef = Atomics.newReference(node);
fn.apply(nodeRef);
AtomicReference<NovaTemplateOptions> optionsRef = Atomics.newReference(options);
AtomicReference<NodeAndNovaTemplateOptions> nodeNovaRef = NodeAndNovaTemplateOptions.newAtomicReference(nodeRef, optionsRef);
fn.apply(nodeNovaRef);
NodeMetadata node1 = nodeRef.get();
assertNotNull(node1);
assertNotNull(optionsRef.get());
assertEquals(node1.getPublicAddresses(), ImmutableSet.of("10.0.0.5"));
}
public void testAllocateWhenAllocationFailsOn404LookupUnusedIpAddToServerAndUpdatesNodeMetadata() throws Exception {
HttpResponse createFloatingIPResponse = HttpResponse
.builder()
.statusCode(404)
.payload(
payloadFromStringWithContentType(
"{\"badRequest\": {\"message\": \"AddressLimitExceeded: Address quota exceeded. You cannot create any more addresses\", \"code\": 404}}",
"application/json")).build();
HttpRequest list = HttpRequest.builder().method("GET").endpoint(
URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/3456/os-floating-ips")).headers(
ImmutableMultimap.<String, String> builder().put("Accept", "application/json").put("X-Auth-Token",
authToken).build()).build();
HttpResponse listResponseForUnassigned = HttpResponse.builder().statusCode(200).payload(
payloadFromResource("/floatingip_list.json")).build();
HttpRequest addFloatingIPRequest = addFloatingIPForAddress("10.0.0.5");
AllocateAndAddFloatingIpToNode fn = requestsSendResponses(
ImmutableMap.<HttpRequest, HttpResponse> builder().put(keystoneAuthWithUsernameAndPasswordAndTenantName,
responseWithKeystoneAccess).put(extensionsOfNovaRequest, extensionsOfNovaResponse).put(
createFloatingIP, createFloatingIPResponse)
.put(addFloatingIPRequest, addFloatingIPResponse).put(list,
listResponseForUnassigned).build()).getContext().utils().injector()
.getInstance(AllocateAndAddFloatingIpToNode.class);
AtomicReference<NodeMetadata> nodeRef = Atomics.newReference(node);
AtomicReference<NovaTemplateOptions> optionsRef = Atomics.newReference(options);
AtomicReference<NodeAndNovaTemplateOptions> nodeNovaRef = NodeAndNovaTemplateOptions.newAtomicReference(nodeRef, optionsRef);
fn.apply(nodeNovaRef);
NodeMetadata node1 = nodeRef.get();
assertNotNull(node1);
assertNotNull(optionsRef.get());
assertEquals(node1.getPublicAddresses(), ImmutableSet.of("10.0.0.5"));
}
}

View File

@ -27,6 +27,10 @@ public class InsufficientResourcesException extends RuntimeException {
super();
}
public InsufficientResourcesException(String arg0) {
super(arg0);
}
public InsufficientResourcesException(String arg0, Throwable arg1) {
super(arg0, arg1);
}