diff --git a/src/main/java/com/netflix/simianarmy/aws/conformity/crawler/AWSClusterCrawler.java b/src/main/java/com/netflix/simianarmy/aws/conformity/crawler/AWSClusterCrawler.java index 92281c09..73343516 100644 --- a/src/main/java/com/netflix/simianarmy/aws/conformity/crawler/AWSClusterCrawler.java +++ b/src/main/java/com/netflix/simianarmy/aws/conformity/crawler/AWSClusterCrawler.java @@ -22,6 +22,7 @@ import com.amazonaws.services.autoscaling.model.SuspendedProcess; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.Cluster; @@ -33,6 +34,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; /** * The class implementing a crawler that gets the auto scaling groups from AWS. @@ -75,11 +77,13 @@ public List clusters(String... clusterNames) { for (Map.Entry entry : regionToAwsClient.entrySet()) { String region = entry.getKey(); AWSClient awsClient = entry.getValue(); + Set asgInstances = Sets.newHashSet(); LOGGER.info(String.format("Crawling clusters in region %s", region)); for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups(clusterNames)) { List instances = Lists.newArrayList(); for (Instance instance : asg.getInstances()) { instances.add(instance.getInstanceId()); + asgInstances.add(instance.getInstanceId()); } com.netflix.simianarmy.conformity.AutoScalingGroup conformityAsg = new com.netflix.simianarmy.conformity.AutoScalingGroup( @@ -94,21 +98,40 @@ public List clusters(String... clusterNames) { } } Cluster cluster = new Cluster(asg.getAutoScalingGroupName(), region, conformityAsg); + updateCluster(cluster); list.add(cluster); - updateExcludedConformityRules(cluster); - cluster.setOwnerEmail(getOwnerEmailForCluster(cluster)); - String prop = String.format("simianarmy.conformity.cluster.%s.optedOut", cluster.getName()); - if (cfg.getBoolOrElse(prop, false)) { - LOGGER.info(String.format("Cluster %s is opted out of Conformity Monkey.", cluster.getName())); - cluster.setOptOutOfConformity(true); - } else { - cluster.setOptOutOfConformity(false); + } + //Cluster containing all solo instances + Set instances = Sets.newHashSet(); + for (com.amazonaws.services.ec2.model.Instance awsInstance : awsClient.describeInstances()) { + if (!asgInstances.contains(awsInstance.getInstanceId())) { + LOGGER.info(String.format("Adding instance %s to soloInstances cluster.", + awsInstance.getInstanceId())); + instances.add(awsInstance.getInstanceId()); } } + //Only create cluster if we have solo instances. + if (!instances.isEmpty()) { + Cluster cluster = new Cluster("SoloInstances", region, instances); + updateCluster(cluster); + list.add(cluster); + } } return list; } + private void updateCluster(Cluster cluster) { + updateExcludedConformityRules(cluster); + cluster.setOwnerEmail(getOwnerEmailForCluster(cluster)); + String prop = String.format("simianarmy.conformity.cluster.%s.optedOut", cluster.getName()); + if (cfg.getBoolOrElse(prop, false)) { + LOGGER.info(String.format("Cluster %s is opted out of Conformity Monkey.", cluster.getName())); + cluster.setOptOutOfConformity(true); + } else { + cluster.setOptOutOfConformity(false); + } + } + /** * Gets the owner email from the monkey configuration. * @param cluster diff --git a/src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceInVPC.java b/src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceInVPC.java new file mode 100644 index 00000000..8d45b895 --- /dev/null +++ b/src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceInVPC.java @@ -0,0 +1,90 @@ +package com.netflix.simianarmy.aws.conformity.rule; + +import com.amazonaws.services.ec2.model.Instance; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The class implements a conformity rule to check an instance is in a virtual private cloud. + */ +public class InstanceInVPC implements ConformityRule { + + private static final Logger LOGGER = LoggerFactory.getLogger(InstanceInVPC.class); + + private final Map regionToAwsClient = Maps.newHashMap(); + private static final String RULE_NAME = "InstanceInVPC"; + private static final String REASON = "VPC_ID not defined"; + + @Override + public Conformity check(Cluster cluster) { + Collection failedComponents = Lists.newArrayList(); + //check all instances + Set failedInstances = checkInstancesInVPC(cluster.getRegion(), cluster.getSoloInstances()); + failedComponents.addAll(failedInstances); + //check asg instances + for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { + if (asg.isSuspended()) { + continue; + } + Set asgFailedInstances = checkInstancesInVPC(cluster.getRegion(), asg.getInstances()); + failedComponents.addAll(asgFailedInstances); + } + return new Conformity(getName(), failedComponents); + } + + @Override + public String getName() { + return RULE_NAME; + } + + @Override + public String getNonconformingReason() { + return REASON; + } + + private AWSClient getAwsClient(String region) { + AWSClient awsClient = regionToAwsClient.get(region); + if (awsClient == null) { + awsClient = new AWSClient(region); + regionToAwsClient.put(region, awsClient); + } + return awsClient; + } + + private Set checkInstancesInVPC(String region, Collection instances) { + Set failedInstances = Sets.newHashSet(); + for (String instanceId : instances) { + for (Instance awsInstance : getAWSInstances(region, instanceId)) { + if (awsInstance.getVpcId() == null) { + LOGGER.info(String.format("Instance %s is not in a virtual private cloud", instanceId)); + failedInstances.add(instanceId); + } + } + } + return failedInstances; + } + + /** + * Gets the list of AWS instances. Can be overridden + * @param region the region + * @param instanceId the instance id. + * @return the list of the AWS instances with the given id. + */ + protected List getAWSInstances(String region, String instanceId) { + AWSClient awsClient = getAwsClient(region); + return awsClient.describeInstances(instanceId); + } +} \ No newline at end of file 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 27e73281..7628e6c1 100644 --- a/src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkeyContext.java +++ b/src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkeyContext.java @@ -29,6 +29,7 @@ import com.netflix.simianarmy.aws.conformity.rule.InstanceHasHealthCheckUrl; import com.netflix.simianarmy.aws.conformity.rule.InstanceHasStatusUrl; import com.netflix.simianarmy.aws.conformity.rule.InstanceInSecurityGroup; +import com.netflix.simianarmy.aws.conformity.rule.InstanceInVPC; import com.netflix.simianarmy.aws.conformity.rule.InstanceIsHealthyInEureka; import com.netflix.simianarmy.aws.conformity.rule.InstanceTooOld; import com.netflix.simianarmy.aws.conformity.rule.SameZonesInElbAndAsg; @@ -143,6 +144,11 @@ public BasicConformityMonkeyContext() { ruleEngine().addRule(new SameZonesInElbAndAsg()); } + if (configuration().getBoolOrElse( + "simianarmy.conformity.rule.InstanceInVPC.enabled", false)) { + ruleEngine.addRule(new InstanceInVPC()); + } + regionToAwsClient.put(region(), new AWSClient(region())); clusterCrawler = new AWSClusterCrawler(regionToAwsClient, configuration()); sesClient = new AmazonSimpleEmailServiceClient(); diff --git a/src/main/java/com/netflix/simianarmy/conformity/Cluster.java b/src/main/java/com/netflix/simianarmy/conformity/Cluster.java index e823ca1e..ba709e5e 100644 --- a/src/main/java/com/netflix/simianarmy/conformity/Cluster.java +++ b/src/main/java/com/netflix/simianarmy/conformity/Cluster.java @@ -30,6 +30,7 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Set; /** * The class implementing clusters. Cluster is the basic unit of conformity check. It can be a single ASG or @@ -55,6 +56,7 @@ public class Cluster { private final Collection excludedConformityRules = Sets.newHashSet(); private boolean isConforming; private boolean isOptOutOfConformity; + private final Set soloInstances = Sets.newHashSet(); /** * Constructor. @@ -74,6 +76,24 @@ public Cluster(String name, String region, AutoScalingGroup... autoScalingGroups } } + /** + * Constructor. + * @param name + * the name of the cluster + * @param soloInstances + * the list of all instances + */ + public Cluster(String name, String region, Set soloInstances) { + Validate.notNull(name); + Validate.notNull(region); + Validate.notNull(soloInstances); + this.name = name; + this.region = region; + for (String soleInstance : soloInstances) { + this.soloInstances.add(soleInstance); + } + } + /** * Gets the name of the cluster. * @return @@ -294,4 +314,9 @@ private static void putToMapIfNotNull(Map map, String key, Strin map.put(key, value); } } + + public Set getSoloInstances() { + return Collections.unmodifiableSet(soloInstances); + } + } diff --git a/src/test/java/com/netflix/simianarmy/aws/conformity/rule/TestInstanceInVPC.java b/src/test/java/com/netflix/simianarmy/aws/conformity/rule/TestInstanceInVPC.java new file mode 100644 index 00000000..c9a5e528 --- /dev/null +++ b/src/test/java/com/netflix/simianarmy/aws/conformity/rule/TestInstanceInVPC.java @@ -0,0 +1,91 @@ +// 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.aws.conformity.rule; + +import com.amazonaws.services.ec2.model.Instance; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.netflix.simianarmy.conformity.AutoScalingGroup; +import com.netflix.simianarmy.conformity.Cluster; +import com.netflix.simianarmy.conformity.Conformity; +import junit.framework.Assert; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Set; + +import static org.mockito.Mockito.doReturn; + + +public class TestInstanceInVPC { + private static final String VPC_INSTANCE_ID = "abc-123"; + private static final String INSTANCE_ID = "zxy-098"; + private static final String REGION = "eu-west-1"; + + @Spy + private InstanceInVPC instanceInVPC = new InstanceInVPC(); + + @BeforeMethod + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + List instanceList = Lists.newArrayList(); + Instance instance = new Instance().withInstanceId(VPC_INSTANCE_ID).withVpcId("12345"); + instanceList.add(instance); + doReturn(instanceList).when(instanceInVPC).getAWSInstances(REGION, VPC_INSTANCE_ID); + List instanceList2 = Lists.newArrayList(); + Instance instance2 = new Instance().withInstanceId(INSTANCE_ID); + instanceList2.add(instance2); + doReturn(instanceList2).when(instanceInVPC).getAWSInstances(REGION, INSTANCE_ID); + + } + + @Test + public void testCheckSoloInstances() throws Exception { + Set list = Sets.newHashSet(); + list.add(VPC_INSTANCE_ID); + list.add(INSTANCE_ID); + Cluster cluster = new Cluster("SoloInstances", REGION, list); + Conformity result = instanceInVPC.check(cluster); + Assert.assertNotNull(result); + Assert.assertEquals(result.getRuleId(), instanceInVPC.getName()); + Assert.assertEquals(result.getFailedComponents().size(), 1); + Assert.assertEquals(result.getFailedComponents().iterator().next(), INSTANCE_ID); + } + + @Test + public void testAsgInstances() throws Exception { + AutoScalingGroup autoScalingGroup = new AutoScalingGroup("Conforming", VPC_INSTANCE_ID); + Cluster conformingCluster = new Cluster("Conforming", REGION, autoScalingGroup); + Conformity result = instanceInVPC.check(conformingCluster); + Assert.assertNotNull(result); + Assert.assertEquals(result.getRuleId(), instanceInVPC.getName()); + Assert.assertEquals(result.getFailedComponents().size(), 0); + + autoScalingGroup = new AutoScalingGroup("NonConforming", INSTANCE_ID); + Cluster nonConformingCluster = new Cluster("NonConforming", REGION, autoScalingGroup); + result = instanceInVPC.check(nonConformingCluster); + Assert.assertNotNull(result); + Assert.assertEquals(result.getRuleId(), instanceInVPC.getName()); + Assert.assertEquals(result.getFailedComponents().size(), 1); + Assert.assertEquals(result.getFailedComponents().iterator().next(), INSTANCE_ID); + } +}