/*
* 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.networking;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import java.net.URI;
import java.util.Collections;
import org.jclouds.aws.AWSResponseException;
import org.jclouds.aws.domain.AWSError;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.domain.SecurityGroup;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.extensions.SecurityGroupExtension;
import org.jclouds.compute.options.TemplateOptions;
import org.jclouds.domain.Location;
import org.jclouds.net.domain.IpPermission;
import org.jclouds.net.domain.IpProtocol;
import org.mockito.Answers;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.apache.brooklyn.location.jclouds.JcloudsLocation;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.net.Cidr;
public class JcloudsLocationSecurityGroupCustomizerTest {
JcloudsLocationSecurityGroupCustomizer customizer;
ComputeService computeService;
Location location;
SecurityGroupExtension securityApi;
/** Used to skip external checks in unit tests. */
private static class TestCidrSupplier implements Supplier<Cidr> {
@Override public Cidr get() {
return new Cidr("192.168.10.10/32");
}
}
@BeforeMethod
public void setUp() {
customizer = new JcloudsLocationSecurityGroupCustomizer("testapp", new TestCidrSupplier());
location = mock(Location.class);
securityApi = mock(SecurityGroupExtension.class);
computeService = mock(ComputeService.class, Answers.RETURNS_DEEP_STUBS.get());
when(computeService.getSecurityGroupExtension()).thenReturn(Optional.of(securityApi));
}
@Test
public void testSameInstanceReturnedForSameApplication() {
assertEquals(JcloudsLocationSecurityGroupCustomizer.getInstance("a"),
JcloudsLocationSecurityGroupCustomizer.getInstance("a"));
assertNotEquals(JcloudsLocationSecurityGroupCustomizer.getInstance("a"),
JcloudsLocationSecurityGroupCustomizer.getInstance("b"));
}
@Test
public void testSecurityGroupAddedWhenJcloudsLocationCustomised() {
Template template = mock(Template.class);
TemplateOptions templateOptions = mock(TemplateOptions.class);
when(template.getLocation()).thenReturn(location);
when(template.getOptions()).thenReturn(templateOptions);
SecurityGroup group = newGroup("id");
when(securityApi.createSecurityGroup(anyString(), eq(location))).thenReturn(group);
// Two Brooklyn.JcloudsLocations added to same Jclouds.Location
JcloudsLocation jcloudsLocationA = new JcloudsLocation(MutableMap.of("deferConstruction", true));
JcloudsLocation jcloudsLocationB = new JcloudsLocation(MutableMap.of("deferConstruction", true));
customizer.customize(jcloudsLocationA, computeService, template);
customizer.customize(jcloudsLocationB, computeService, template);
// One group with three permissions shared by both locations.
// Expect TCP, UDP and ICMP between members of group and SSH to Brooklyn
verify(securityApi).createSecurityGroup(anyString(), eq(location));
verify(securityApi, times(4)).addIpPermission(any(IpPermission.class), eq(group));
// New groups set on options
verify(templateOptions, times(2)).securityGroups(anyString());
}
@Test
public void testSharedGroupLoadedWhenItExistsButIsNotCached() {
Template template = mock(Template.class);
TemplateOptions templateOptions = mock(TemplateOptions.class);
when(template.getLocation()).thenReturn(location);
when(template.getOptions()).thenReturn(templateOptions);
JcloudsLocation jcloudsLocation = new JcloudsLocation(MutableMap.of("deferConstruction", true));
SecurityGroup shared = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup irrelevant = newGroup("irrelevant");
when(securityApi.listSecurityGroupsInLocation(location)).thenReturn(ImmutableSet.of(irrelevant, shared));
customizer.customize(jcloudsLocation, computeService, template);
verify(securityApi).listSecurityGroupsInLocation(location);
verify(securityApi, never()).createSecurityGroup(anyString(), any(Location.class));
}
@Test
public void testAddPermissionsToNode() {
IpPermission ssh = newPermission(22);
IpPermission jmx = newPermission(31001);
String nodeId = "node";
SecurityGroup sharedGroup = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup group = newGroup("id");
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(ImmutableSet.of(sharedGroup, group));
when(computeService.getContext().unwrap().getId()).thenReturn("aws-ec2");
customizer.addPermissionsToLocation(ImmutableList.of(ssh, jmx), nodeId, computeService);
verify(securityApi, never()).createSecurityGroup(anyString(), any(Location.class));
verify(securityApi, times(1)).addIpPermission(ssh, group);
verify(securityApi, times(1)).addIpPermission(jmx, group);
}
@Test
public void testRemovePermissionsFromNode() {
IpPermission ssh = newPermission(22);
IpPermission jmx = newPermission(31001);
String nodeId = "node";
SecurityGroup sharedGroup = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup group = newGroup("id");
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(ImmutableSet.of(sharedGroup, group));
when(computeService.getContext().unwrap().getId()).thenReturn("aws-ec2");
customizer.addPermissionsToLocation(ImmutableList.of(ssh, jmx), nodeId, computeService);
customizer.removePermissionsFromLocation(ImmutableList.of(jmx), nodeId, computeService);
verify(securityApi, never()).removeIpPermission(ssh, group);
verify(securityApi, times(1)).removeIpPermission(jmx, group);
}
@Test
public void testRemoveMultiplePermissionsFromNode() {
IpPermission ssh = newPermission(22);
IpPermission jmx = newPermission(31001);
String nodeId = "node";
SecurityGroup sharedGroup = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup group = newGroup("id");
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(ImmutableSet.of(sharedGroup, group));
when(computeService.getContext().unwrap().getId()).thenReturn("aws-ec2");
customizer.addPermissionsToLocation(ImmutableList.of(ssh, jmx), nodeId, computeService);
customizer.removePermissionsFromLocation(ImmutableList.of(ssh, jmx), nodeId, computeService);
verify(securityApi, times(1)).removeIpPermission(ssh, group);
verify(securityApi, times(1)).removeIpPermission(jmx, group);
}
@Test
public void testAddPermissionWhenNoExtension() {
IpPermission ssh = newPermission(22);
IpPermission jmx = newPermission(31001);
String nodeId = "node";
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(Collections.<SecurityGroup>emptySet());
RuntimeException exception = null;
try {
customizer.addPermissionsToLocation(ImmutableList.of(ssh, jmx), nodeId, computeService);
} catch(RuntimeException e){
exception = e;
}
assertNotNull(exception);
}
@Test
public void testAddPermissionsToNodeUsesUncachedSecurityGroup() {
JcloudsLocation jcloudsLocation = new JcloudsLocation(MutableMap.of("deferConstruction", true));
IpPermission ssh = newPermission(22);
String nodeId = "nodeId";
SecurityGroup sharedGroup = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup uniqueGroup = newGroup("unique");
Template template = mock(Template.class);
TemplateOptions templateOptions = mock(TemplateOptions.class);
when(template.getLocation()).thenReturn(location);
when(template.getOptions()).thenReturn(templateOptions);
when(securityApi.createSecurityGroup(anyString(), eq(location))).thenReturn(sharedGroup);
when(computeService.getContext().unwrap().getId()).thenReturn("aws-ec2");
// Call customize to cache the shared group
customizer.customize(jcloudsLocation, computeService, template);
reset(securityApi);
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(ImmutableSet.of(uniqueGroup, sharedGroup));
customizer.addPermissionsToLocation(ImmutableSet.of(ssh), nodeId, computeService);
// Expect the per-machine group to have been altered, not the shared group
verify(securityApi).addIpPermission(ssh, uniqueGroup);
verify(securityApi, never()).addIpPermission(any(IpPermission.class), eq(sharedGroup));
}
@Test
public void testSecurityGroupsLoadedWhenAddingPermissionsToUncachedNode() {
IpPermission ssh = newPermission(22);
String nodeId = "nodeId";
SecurityGroup sharedGroup = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup uniqueGroup = newGroup("unique");
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(ImmutableSet.of(sharedGroup, uniqueGroup));
when(computeService.getContext().unwrap().getId()).thenReturn("aws-ec2");
// Expect first call to list security groups on nodeId, second to use cached version
customizer.addPermissionsToLocation(ImmutableSet.of(ssh), nodeId, computeService);
customizer.addPermissionsToLocation(ImmutableSet.of(ssh), nodeId, computeService);
verify(securityApi, times(1)).listSecurityGroupsForNode(nodeId);
verify(securityApi, times(2)).addIpPermission(ssh, uniqueGroup);
verify(securityApi, never()).addIpPermission(any(IpPermission.class), eq(sharedGroup));
}
@Test
public void testAddRuleNotRetriedByDefault() {
IpPermission ssh = newPermission(22);
String nodeId = "node";
SecurityGroup sharedGroup = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup uniqueGroup = newGroup("unique");
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(ImmutableSet.of(sharedGroup, uniqueGroup));
when(securityApi.addIpPermission(eq(ssh), eq(uniqueGroup)))
.thenThrow(new RuntimeException("exception creating " + ssh));
when(computeService.getContext().unwrap().getId()).thenReturn("aws-ec2");
try {
customizer.addPermissionsToLocation(ImmutableList.of(ssh), nodeId, computeService);
} catch (Exception e) {
assertTrue(e.getMessage().contains("repeated errors from provider"), "message=" + e.getMessage());
}
verify(securityApi, never()).createSecurityGroup(anyString(), any(Location.class));
verify(securityApi, times(1)).addIpPermission(ssh, uniqueGroup);
}
@Test
public void testCustomExceptionRetryablePredicate() {
final String message = "testCustomExceptionRetryablePredicate";
Predicate<Exception> messageChecker = new Predicate<Exception>() {
@Override
public boolean apply(Exception input) {
Throwable t = input;
while (t != null) {
if (t.getMessage().contains(message)) {
return true;
} else {
t = t.getCause();
}
}
return false;
}
};
customizer.setRetryExceptionPredicate(messageChecker);
when(computeService.getContext().unwrap().getId()).thenReturn("aws-ec2");
IpPermission ssh = newPermission(22);
String nodeId = "node";
SecurityGroup sharedGroup = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup uniqueGroup = newGroup("unique");
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(ImmutableSet.of(sharedGroup, uniqueGroup));
when(securityApi.addIpPermission(eq(ssh), eq(uniqueGroup)))
.thenThrow(new RuntimeException(new Exception(message)))
.thenThrow(new RuntimeException(new Exception(message)))
.thenReturn(sharedGroup);
customizer.addPermissionsToLocation(ImmutableList.of(ssh), nodeId, computeService);
verify(securityApi, never()).createSecurityGroup(anyString(), any(Location.class));
verify(securityApi, times(3)).addIpPermission(ssh, uniqueGroup);
}
@Test
public void testAddRuleRetriedOnAwsFailure() {
IpPermission ssh = newPermission(22);
String nodeId = "nodeId";
SecurityGroup sharedGroup = newGroup(customizer.getNameForSharedSecurityGroup());
SecurityGroup uniqueGroup = newGroup("unique");
customizer.setRetryExceptionPredicate(JcloudsLocationSecurityGroupCustomizer.newAwsExceptionRetryPredicate());
when(securityApi.listSecurityGroupsForNode(nodeId)).thenReturn(ImmutableSet.of(sharedGroup, uniqueGroup));
when(securityApi.addIpPermission(any(IpPermission.class), eq(uniqueGroup)))
.thenThrow(newAwsResponseExceptionWithCode("InvalidGroup.InUse"))
.thenThrow(newAwsResponseExceptionWithCode("DependencyViolation"))
.thenThrow(newAwsResponseExceptionWithCode("RequestLimitExceeded"))
.thenThrow(newAwsResponseExceptionWithCode("Blocked"))
.thenReturn(sharedGroup);
when(computeService.getContext().unwrap().getId()).thenReturn("aws-ec2");
try {
customizer.addPermissionsToLocation(ImmutableList.of(ssh), nodeId, computeService);
} catch (Exception e) {
String expected = "repeated errors from provider";
assertTrue(e.getMessage().contains(expected), "expected exception message to contain " + expected + ", was: " + e.getMessage());
}
verify(securityApi, never()).createSecurityGroup(anyString(), any(Location.class));
verify(securityApi, times(4)).addIpPermission(ssh, uniqueGroup);
}
private SecurityGroup newGroup(String id) {
URI uri = null;
String ownerId = null;
return new SecurityGroup(
"providerId",
id,
id,
location,
uri,
Collections.<String, String>emptyMap(),
ImmutableSet.<String>of(),
ImmutableSet.<IpPermission>of(),
ownerId);
}
private IpPermission newPermission(int port) {
return IpPermission.builder()
.ipProtocol(IpProtocol.TCP)
.fromPort(port)
.toPort(port)
.cidrBlock("0.0.0.0/0")
.build();
}
private AWSError newAwsErrorWithCode(String code) {
AWSError e = new AWSError();
e.setCode(code);
return e;
}
private Exception newAwsResponseExceptionWithCode(String code) {
AWSResponseException e = new AWSResponseException("irrelevant message", null, null, newAwsErrorWithCode(code));
return new RuntimeException(e);
}
}