diff --git a/src/main/java/com/netflix/simianarmy/aws/conformity/rule/CrossZoneLoadBalancing.java b/src/main/java/com/netflix/simianarmy/aws/conformity/rule/CrossZoneLoadBalancing.java new file mode 100644 index 00000000..dcacd983 --- /dev/null +++ b/src/main/java/com/netflix/simianarmy/aws/conformity/rule/CrossZoneLoadBalancing.java @@ -0,0 +1,133 @@ +/* + * + * Copyright 2013 Netflix, Inc. + * + * Licensed 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 com.netflix.simianarmy.aws.conformity.rule; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerAttributes; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.netflix.simianarmy.client.aws.AWSClient; +import com.netflix.simianarmy.conformity.AutoScalingGroup; +import com.netflix.simianarmy.conformity.Cluster; +import com.netflix.simianarmy.conformity.Conformity; +import com.netflix.simianarmy.conformity.ConformityRule; + +/** + * The class implementing a conformity rule that checks if the cross-zone load balancing is enabled + * for all cluster ELBs. + */ +public class CrossZoneLoadBalancing implements ConformityRule { + private static final Logger LOGGER = LoggerFactory.getLogger(CrossZoneLoadBalancing.class); + + private final Map regionToAwsClient = Maps.newHashMap(); + + private AWSCredentialsProvider awsCredentialsProvider; + + private static final String RULE_NAME = "CrossZoneLoadBalancing"; + private static final String REASON = "Cross-zone load balancing is disabled"; + + /** + * Constructs an instance with the default AWS credentials provider chain. + * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain + */ + public CrossZoneLoadBalancing() { + this(new DefaultAWSCredentialsProviderChain()); + } + + /** + * Constructs an instance with the passed AWS Credential Provider. + * @param awsCredentialsProvider + */ + public CrossZoneLoadBalancing(AWSCredentialsProvider awsCredentialsProvider) { + this.awsCredentialsProvider = awsCredentialsProvider; + } + + @Override + public Conformity check(Cluster cluster) { + Collection failedComponents = Lists.newArrayList(); + for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { + for (String lbName : getLoadBalancerNamesForAsg(cluster.getRegion(), asg.getName())) { + if (!isCrossZoneLoadBalancingEnabled(cluster.getRegion(), lbName)) { + LOGGER.info(String.format("ELB %s in %s does not have cross-zone load balancing enabled", + lbName, cluster.getRegion())); + failedComponents.add(lbName); + } + } + } + return new Conformity(getName(), failedComponents); + } + + /** + * Gets the cross-zone load balancing option for an ELB. Can be overridden in subclasses. + * @param region the region + * @param lbName the ELB name + * @return {@code true} if cross-zone load balancing is enabled + */ + protected boolean isCrossZoneLoadBalancingEnabled(String region, String lbName) { + LoadBalancerAttributes attrs = getAwsClient(region).describeElasticLoadBalancerAttributes(lbName); + return attrs.getCrossZoneLoadBalancing().isEnabled(); + } + + @Override + public String getName() { + return RULE_NAME; + } + + @Override + public String getNonconformingReason() { + return REASON; + } + + /** + * Gets the load balancer names of an ASG. Can be overridden in subclasses. + * @param region the region + * @param asgName the ASG name + * @return the list of load balancer names + */ + protected List getLoadBalancerNamesForAsg(String region, String asgName) { + List asgs = + getAwsClient(region).describeAutoScalingGroups(asgName); + if (asgs.isEmpty()) { + LOGGER.error(String.format("Not found ASG with name %s", asgName)); + return Collections.emptyList(); + } else { + return asgs.get(0).getLoadBalancerNames(); + } + } + + private AWSClient getAwsClient(String region) { + AWSClient awsClient = regionToAwsClient.get(region); + if (awsClient == null) { + awsClient = new AWSClient(region, awsCredentialsProvider); + regionToAwsClient.put(region, awsClient); + } + return awsClient; + } + + + +} diff --git a/src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkeyContext.java b/src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkeyContext.java index 92e9e8c4..e08c9983 100644 --- a/src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkeyContext.java +++ b/src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkeyContext.java @@ -26,6 +26,7 @@ import com.netflix.simianarmy.aws.conformity.crawler.AWSClusterCrawler; import com.netflix.simianarmy.aws.conformity.rule.BasicConformityEurekaClient; import com.netflix.simianarmy.aws.conformity.rule.ConformityEurekaClient; +import com.netflix.simianarmy.aws.conformity.rule.CrossZoneLoadBalancing; import com.netflix.simianarmy.aws.conformity.rule.InstanceHasHealthCheckUrl; import com.netflix.simianarmy.aws.conformity.rule.InstanceHasStatusUrl; import com.netflix.simianarmy.aws.conformity.rule.InstanceInSecurityGroup; @@ -42,6 +43,7 @@ import com.netflix.simianarmy.conformity.ConformityMonkey; import com.netflix.simianarmy.conformity.ConformityRule; import com.netflix.simianarmy.conformity.ConformityRuleEngine; + import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -150,6 +152,11 @@ public BasicConformityMonkeyContext() { ruleEngine.addRule(new InstanceInVPC(getAwsCredentialsProvider())); } + if (configuration().getBoolOrElse( + "simianarmy.conformity.rule.CrossZoneLoadBalancing.enabled", false)) { + ruleEngine().addRule(new CrossZoneLoadBalancing(getAwsCredentialsProvider())); + } + createClient(region()); regionToAwsClient.put(region(), awsClient()); diff --git a/src/main/java/com/netflix/simianarmy/client/aws/AWSClient.java b/src/main/java/com/netflix/simianarmy/client/aws/AWSClient.java index a8cea238..baac755b 100644 --- a/src/main/java/com/netflix/simianarmy/client/aws/AWSClient.java +++ b/src/main/java/com/netflix/simianarmy/client/aws/AWSClient.java @@ -62,8 +62,11 @@ import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import com.amazonaws.services.ec2.model.Volume; import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingClient; +import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancerAttributesRequest; +import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancerAttributesResult; import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersRequest; import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersResult; +import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerAttributes; import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription; import com.amazonaws.services.simpledb.AmazonSimpleDB; import com.amazonaws.services.simpledb.AmazonSimpleDBClient; @@ -306,7 +309,22 @@ public List describeElasticLoadBalancers(String... name return elbs; } - + /** + * Describe a set of specific ELBs. + * + * @param names the ELB names + * @return the ELBs + */ + public LoadBalancerAttributes describeElasticLoadBalancerAttributes(String name) { + LOGGER.info(String.format("Getting attributes for ELB with name '%s' in region %s.", name, region)); + AmazonElasticLoadBalancingClient elbClient = elbClient(); + DescribeLoadBalancerAttributesRequest request = new DescribeLoadBalancerAttributesRequest().withLoadBalancerName(name); + DescribeLoadBalancerAttributesResult result = elbClient.describeLoadBalancerAttributes(request); + LoadBalancerAttributes attrs = result.getLoadBalancerAttributes(); + LOGGER.info(String.format("Got attributes for ELB with name '%s' in region %s.", name, region)); + return attrs; + } + /** * Describe a set of specific auto-scaling instances. * diff --git a/src/test/java/com/netflix/simianarmy/conformity/TestCrossZoneLoadBalancing.java b/src/test/java/com/netflix/simianarmy/conformity/TestCrossZoneLoadBalancing.java new file mode 100644 index 00000000..acfc1cd4 --- /dev/null +++ b/src/test/java/com/netflix/simianarmy/conformity/TestCrossZoneLoadBalancing.java @@ -0,0 +1,83 @@ +// CHECKSTYLE IGNORE Javadoc +/* + * + * Copyright 2013 Netflix, Inc. + * + * Licensed 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 com.netflix.simianarmy.conformity; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import junit.framework.Assert; + +import org.apache.commons.lang.StringUtils; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.google.common.collect.Maps; +import com.netflix.simianarmy.aws.conformity.rule.CrossZoneLoadBalancing; + +public class TestCrossZoneLoadBalancing extends CrossZoneLoadBalancing { + private final Map asgToElbs = Maps.newHashMap(); + private final Map elbsToCZLB = Maps.newHashMap(); + + @BeforeClass + private void init() { + asgToElbs.put("asg1", "elb1,elb2"); + asgToElbs.put("asg2", "elb1"); + asgToElbs.put("asg3", ""); + elbsToCZLB.put("elb1", true); + } + + @Test + public void testDisabledCrossZoneLoadBalancing() { + Cluster cluster = new Cluster("cluster1", "us-east-1", new AutoScalingGroup("asg1")); + Conformity result = check(cluster); + Assert.assertEquals(result.getRuleId(), getName()); + Assert.assertEquals(result.getFailedComponents().size(), 1); + Assert.assertEquals(result.getFailedComponents().iterator().next(), "elb2"); + } + + @Test + public void testEnabledCrossZoneLoadBalancing() { + Cluster cluster = new Cluster("cluster1", "us-east-1", new AutoScalingGroup("asg2")); + Conformity result = check(cluster); + Assert.assertEquals(result.getRuleId(), getName()); + Assert.assertEquals(result.getFailedComponents().size(), 0); + } + + @Test + public void testAsgWithoutElb() { + Cluster cluster = new Cluster("cluster3", "us-east-1", new AutoScalingGroup("asg3")); + Conformity result = check(cluster); + Assert.assertEquals(result.getRuleId(), getName()); + Assert.assertEquals(result.getFailedComponents().size(), 0); + } + + @Override + protected List getLoadBalancerNamesForAsg(String region, String asgName) { + return Arrays.asList(StringUtils.split(asgToElbs.get(asgName), ",")); + } + + @Override + protected boolean isCrossZoneLoadBalancingEnabled(String region, String lbName) { + Boolean enabled = elbsToCZLB.get(lbName); + return (enabled == null) ? false : enabled; + } + + +}