diff --git a/compute/src/main/java/org/jclouds/compute/util/ComputeServiceUtils.java b/compute/src/main/java/org/jclouds/compute/util/ComputeServiceUtils.java index b13785b5bb..1890bf59ca 100644 --- a/compute/src/main/java/org/jclouds/compute/util/ComputeServiceUtils.java +++ b/compute/src/main/java/org/jclouds/compute/util/ComputeServiceUtils.java @@ -29,6 +29,7 @@ import java.util.Formatter; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; +import java.util.Set; import java.util.regex.Pattern; import org.jclouds.compute.domain.ComputeMetadata; @@ -44,12 +45,19 @@ import org.jclouds.http.HttpRequest; import org.jclouds.scriptbuilder.domain.Statement; import org.jclouds.scriptbuilder.domain.Statements; +import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; +import com.google.common.primitives.Ints; /** * @@ -257,4 +265,21 @@ public class ComputeServiceUtils { } } + public static Map getPortRangesFromList(int... ports) { + Set sortedPorts = ImmutableSortedSet.copyOf(Ints.asList(ports)); + + RangeSet ranges = TreeRangeSet.create(); + + for (Integer port : sortedPorts) { + ranges.add(Range.closedOpen(port, port + 1)); + } + + Map portRanges = Maps.newHashMap(); + + for (Range r : ranges.asRanges()) { + portRanges.put(r.lowerEndpoint(), r.upperEndpoint() - 1); + } + + return portRanges; + } } diff --git a/compute/src/test/java/org/jclouds/compute/util/ComputeServiceUtilsTest.java b/compute/src/test/java/org/jclouds/compute/util/ComputeServiceUtilsTest.java index 5f4e4b2dd9..0f86f6a8cd 100644 --- a/compute/src/test/java/org/jclouds/compute/util/ComputeServiceUtilsTest.java +++ b/compute/src/test/java/org/jclouds/compute/util/ComputeServiceUtilsTest.java @@ -41,6 +41,7 @@ import org.testng.annotations.Test; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; import com.google.inject.Guice; /** @@ -141,4 +142,13 @@ public class ComputeServiceUtilsTest { org.jclouds.scriptbuilder.domain.OsFamily.UNIX), "curl -q -s -S -L --connect-timeout 10 --max-time 600 --retry 20 -X GET -H \"Host: adriancolehappy.s3.amazonaws.com\" -H \"Date: Sun, 12 Sep 2010 08:25:19 GMT\" -H \"Authorization: AWS 0ASHDJAS82:JASHFDA=\" https://adriancolehappy.s3.amazonaws.com/java/install |(mkdir -p /stage/ &&cd /stage/ &&tar -xpzf -)\n"); } + + @Test + public void testGetPortRangesFromList() { + Map portRanges = Maps.newHashMap(); + portRanges.put(5, 7); + portRanges.put(10, 11); + portRanges.put(20, 20); + assertEquals(portRanges, ComputeServiceUtils.getPortRangesFromList(5, 6, 7, 10, 11, 20)); + } } diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceDependenciesModule.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceDependenciesModule.java index 89e9bf4ff4..f82216f215 100644 --- a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceDependenciesModule.java +++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/config/AWSEC2ComputeServiceDependenciesModule.java @@ -31,6 +31,7 @@ import javax.inject.Singleton; import org.jclouds.aws.ec2.compute.AWSEC2ComputeService; import org.jclouds.aws.ec2.compute.AWSEC2TemplateOptions; +import org.jclouds.aws.ec2.compute.loaders.AWSEC2CreateSecurityGroupIfNeeded; import org.jclouds.aws.ec2.compute.suppliers.CallForImages; import org.jclouds.aws.ec2.domain.PlacementGroup; import org.jclouds.aws.ec2.domain.RegionNameAndPublicKeyMaterial; @@ -55,7 +56,6 @@ import org.jclouds.ec2.compute.functions.EC2ImageParser; import org.jclouds.ec2.compute.functions.PasswordCredentialsFromWindowsInstance; import org.jclouds.ec2.compute.functions.WindowsLoginCredentialsFromEncryptedData; import org.jclouds.ec2.compute.internal.EC2TemplateBuilderImpl; -import org.jclouds.ec2.compute.loaders.CreateSecurityGroupIfNeeded; import org.jclouds.ec2.compute.loaders.LoadPublicIpForInstanceOrNull; import org.jclouds.ec2.compute.loaders.RegionAndIdToImage; import org.jclouds.ec2.domain.KeyPair; @@ -89,7 +89,7 @@ public class AWSEC2ComputeServiceDependenciesModule extends EC2ComputeServiceDep bind(new TypeLiteral>>() { }).to(CredentialsForInstance.class); bind(new TypeLiteral>() { - }).annotatedWith(Names.named("SECURITY")).to(CreateSecurityGroupIfNeeded.class); + }).annotatedWith(Names.named("SECURITY")).to(AWSEC2CreateSecurityGroupIfNeeded.class); bind(new TypeLiteral>() { }).annotatedWith(Names.named("ELASTICIP")).to(LoadPublicIpForInstanceOrNull.class); bind(new TypeLiteral>() { diff --git a/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/loaders/AWSEC2CreateSecurityGroupIfNeeded.java b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/loaders/AWSEC2CreateSecurityGroupIfNeeded.java new file mode 100644 index 0000000000..ad14efedd4 --- /dev/null +++ b/providers/aws-ec2/src/main/java/org/jclouds/aws/ec2/compute/loaders/AWSEC2CreateSecurityGroupIfNeeded.java @@ -0,0 +1,130 @@ +/* + * 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.aws.ec2.compute.loaders; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.jclouds.compute.util.ComputeServiceUtils.getPortRangesFromList; + +import java.util.Map; +import java.util.Set; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.jclouds.aws.ec2.AWSEC2Client; +import org.jclouds.aws.ec2.services.AWSSecurityGroupClient; +import org.jclouds.compute.reference.ComputeServiceConstants; +import org.jclouds.ec2.compute.domain.RegionAndName; +import org.jclouds.ec2.compute.domain.RegionNameAndIngressRules; +import org.jclouds.ec2.domain.IpPermission; +import org.jclouds.ec2.domain.IpProtocol; +import org.jclouds.ec2.domain.UserIdGroupPair; +import org.jclouds.logging.Logger; + +import com.google.common.base.Predicate; +import com.google.common.cache.CacheLoader; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +/** + * + * @author Adrian Cole + * @author Andrew Bayer + */ +@Singleton +public class AWSEC2CreateSecurityGroupIfNeeded extends CacheLoader { + @Resource + @Named(ComputeServiceConstants.COMPUTE_LOGGER) + protected Logger logger = Logger.NULL; + protected final AWSSecurityGroupClient securityClient; + protected final Predicate securityGroupEventualConsistencyDelay; + + @Inject + public AWSEC2CreateSecurityGroupIfNeeded(AWSEC2Client ec2Client, + @Named("SECURITY") Predicate securityGroupEventualConsistencyDelay) { + this(checkNotNull(ec2Client, "ec2Client").getSecurityGroupServices(), securityGroupEventualConsistencyDelay); + } + + public AWSEC2CreateSecurityGroupIfNeeded(AWSSecurityGroupClient securityClient, + @Named("SECURITY") Predicate securityGroupEventualConsistencyDelay) { + this.securityClient = checkNotNull(securityClient, "securityClient"); + this.securityGroupEventualConsistencyDelay = checkNotNull(securityGroupEventualConsistencyDelay, + "securityGroupEventualConsistencyDelay"); + } + + @Override + public String load(RegionAndName from) { + RegionNameAndIngressRules realFrom = RegionNameAndIngressRules.class.cast(from); + createSecurityGroupInRegion(from.getRegion(), from.getName(), realFrom.getPorts()); + return from.getName(); + } + + private void createSecurityGroupInRegion(String region, String name, int... ports) { + checkNotNull(region, "region"); + checkNotNull(name, "name"); + logger.debug(">> creating securityGroup region(%s) name(%s)", region, name); + try { + securityClient.createSecurityGroupInRegion(region, name, name); + boolean created = securityGroupEventualConsistencyDelay.apply(new RegionAndName(region, name)); + if (!created) + throw new RuntimeException(String.format("security group %s/%s is not available after creating", region, + name)); + logger.debug("<< created securityGroup(%s)", name); + + ImmutableSet.Builder permissions = ImmutableSet.builder(); + + if (ports.length > 0) { + for (Map.Entry range : getPortRangesFromList(ports).entrySet()) { + permissions.add(IpPermission.builder() + .fromPort(range.getKey()) + .toPort(range.getValue()) + .ipProtocol(IpProtocol.TCP) + .ipRange("0.0.0.0/0") + .build()); + } + + String myOwnerId = Iterables.get(securityClient.describeSecurityGroupsInRegion(region, name), 0).getOwnerId(); + permissions.add(IpPermission.builder() + .fromPort(0) + .toPort(65535) + .ipProtocol(IpProtocol.TCP) + .userIdGroupPair(myOwnerId, name) + .build()); + permissions.add(IpPermission.builder() + .fromPort(0) + .toPort(65535) + .ipProtocol(IpProtocol.UDP) + .userIdGroupPair(myOwnerId, name) + .build()); + } + + Set perms = permissions.build(); + + if (perms.size() > 0) { + logger.debug(">> authorizing securityGroup region(%s) name(%s) IpPermissions(%s)", region, name, perms); + securityClient.authorizeSecurityGroupIngressInRegion(region, name, perms); + logger.debug("<< authorized securityGroup(%s)", name); + } + + } catch (IllegalStateException e) { + logger.debug("<< reused securityGroup(%s)", name); + } + } + +} diff --git a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/internal/BaseAWSEC2ComputeServiceExpectTest.java b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/internal/BaseAWSEC2ComputeServiceExpectTest.java index 95b485a97f..193cd72a00 100644 --- a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/internal/BaseAWSEC2ComputeServiceExpectTest.java +++ b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/internal/BaseAWSEC2ComputeServiceExpectTest.java @@ -59,6 +59,29 @@ public abstract class BaseAWSEC2ComputeServiceExpectTest extends BaseEC2ComputeS protected void setupDefaultRequests() { super.setupDefaultRequests(); + authorizeSecurityGroupIngressRequest22 = + formSigner.filter(HttpRequest.builder() + .method("POST") + .endpoint("https://ec2." + region + ".amazonaws.com/") + .addHeader("Host", "ec2." + region + ".amazonaws.com") + .addFormParam("Action", "AuthorizeSecurityGroupIngress") + .addFormParam("GroupId", "jclouds#test") + .addFormParam("IpPermissions.0.FromPort", "22") + .addFormParam("IpPermissions.0.ToPort", "22") + .addFormParam("IpPermissions.0.IpRanges.0.CidrIp", "0.0.0.0/0") + .addFormParam("IpPermissions.0.IpProtocol", "tcp") + .addFormParam("IpPermissions.1.FromPort", "0") + .addFormParam("IpPermissions.1.ToPort", "65535") + .addFormParam("IpPermissions.1.Groups.0.GroupName", "jclouds#test") + .addFormParam("IpPermissions.1.Groups.0.UserId", "993194456877") + .addFormParam("IpPermissions.1.IpProtocol", "tcp") + .addFormParam("IpPermissions.2.FromPort", "0") + .addFormParam("IpPermissions.2.ToPort", "65535") + .addFormParam("IpPermissions.2.Groups.0.GroupName", "jclouds#test") + .addFormParam("IpPermissions.2.Groups.0.UserId", "993194456877") + .addFormParam("IpPermissions.2.IpProtocol", "udp") + .build()); + describeImagesRequest = formSigner.filter(HttpRequest.builder() .method("POST") diff --git a/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/loaders/AWSEC2CreateSecurityGroupIfNeededTest.java b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/loaders/AWSEC2CreateSecurityGroupIfNeededTest.java new file mode 100644 index 0000000000..db3093c2af --- /dev/null +++ b/providers/aws-ec2/src/test/java/org/jclouds/aws/ec2/compute/loaders/AWSEC2CreateSecurityGroupIfNeededTest.java @@ -0,0 +1,99 @@ +/* + * 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.aws.ec2.compute.loaders; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.testng.Assert.assertEquals; + +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import org.jclouds.aws.ec2.services.AWSSecurityGroupClient; +import org.jclouds.ec2.compute.domain.RegionAndName; +import org.jclouds.ec2.compute.domain.RegionNameAndIngressRules; +import org.jclouds.ec2.domain.IpPermission; +import org.jclouds.ec2.domain.IpProtocol; +import org.jclouds.ec2.domain.SecurityGroup; +import org.jclouds.ec2.domain.UserIdGroupPair; +import org.testng.annotations.Test; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableSet; + +/** + * @author Adrian Cole + * @author Andrew Bayer + */ +@Test(groups = "unit", singleThreaded = true, testName = "AWSEC2CreateSecurityGroupIfNeeded") +public class AWSEC2CreateSecurityGroupIfNeededTest { + + @SuppressWarnings("unchecked") + @Test + public void testWhenPort22AndToItselfAuthorizesIngressOnce() throws ExecutionException { + + AWSSecurityGroupClient client = createMock(AWSSecurityGroupClient.class); + Predicate tester = Predicates.alwaysTrue(); + + SecurityGroup group = createNiceMock(SecurityGroup.class); + Set groups = ImmutableSet. of(group); + + ImmutableSet.Builder permissions = ImmutableSet.builder(); + + permissions.add(IpPermission.builder() + .fromPort(22) + .toPort(22) + .ipProtocol(IpProtocol.TCP) + .ipRange("0.0.0.0/0") + .build()); + + permissions.add(IpPermission.builder() + .fromPort(0) + .toPort(65535) + .ipProtocol(IpProtocol.TCP) + .userIdGroupPair("ownerId", "group") + .build()); + permissions.add(IpPermission.builder() + .fromPort(0) + .toPort(65535) + .ipProtocol(IpProtocol.UDP) + .userIdGroupPair("ownerId", "group") + .build()); + + client.createSecurityGroupInRegion("region", "group", "group"); + expect(group.getOwnerId()).andReturn("ownerId"); + client.authorizeSecurityGroupIngressInRegion("region", "group", permissions.build()); + expect(client.describeSecurityGroupsInRegion("region", "group")).andReturn(Set.class.cast(groups)); + + + replay(client); + replay(group); + + AWSEC2CreateSecurityGroupIfNeeded function = new AWSEC2CreateSecurityGroupIfNeeded(client, tester); + + assertEquals("group", function.load(new RegionNameAndIngressRules("region", "group", new int[] { 22 }, true))); + + verify(client); + verify(group); + + } +}