diff --git a/build.gradle b/build.gradle
index 2123afea..8d158adc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ apply plugin:'eclipse-wtp'
buildscript {
repositories { mavenCentral() }
- apply from: file('gradle/buildscript.gradle'), to: buildscript
+ apply from: file('gradle/buildscript.gradle'), to: buildscript
}
allprojects {
@@ -23,7 +23,7 @@ apply plugin: 'jetty'
apply plugin: 'eclipse'
dependencies {
- // for the VMWareClient
+ // for the VMWareClient
compile 'com.cloudbees.thirdparty:vijava:5.0.0'
compile 'dom4j:dom4j:1.6.1'
@@ -34,10 +34,12 @@ dependencies {
compile 'org.slf4j:slf4j-api:1.6.4'
compile 'org.codehaus.jackson:jackson-core-asl:1.9.2'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.2'
+ compile 'com.netflix.eureka:eureka-client:1.1.22'
compile('com.amazonaws:aws-java-sdk:1.3.11') {
exclude group:'org.codehaus.jackson'
}
compile 'commons-lang:commons-lang:2.6'
+ compile 'joda-time:joda-time:2.1'
testCompile 'org.testng:testng:6.3.1'
testCompile 'org.mockito:mockito-core:1.8.5'
@@ -53,12 +55,12 @@ dependencies {
}
test {
- useTestNG()
+ useTestNG()
}
tasks.withType(Compile) {
options.compilerArgs << "-Xlint" << "-Werror"
-}
+}
import nl.javadude.gradle.plugins.license.License
tasks.withType(License).each { licenseTask ->
diff --git a/gradle.properties b/gradle.properties
index 67f32e54..d70e3778 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1 @@
-version=2.3-SNAPSHOT
+version=2.4-SNAPSHOT
diff --git a/src/main/java/com/netflix/simianarmy/AbstractEmailBuilder.java b/src/main/java/com/netflix/simianarmy/AbstractEmailBuilder.java
new file mode 100644
index 00000000..107eff43
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/AbstractEmailBuilder.java
@@ -0,0 +1,83 @@
+/*
+ *
+ * Copyright 2012 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;
+
+/** The abstract email builder. */
+public abstract class AbstractEmailBuilder implements EmailBuilder {
+
+ @Override
+ public String buildEmailBody(String emailAddress) {
+ StringBuilder body = new StringBuilder();
+ String header = getHeader();
+ if (header != null) {
+ body.append(header);
+ }
+ String entryTable = getEntryTable(emailAddress);
+ if (entryTable != null) {
+ body.append(entryTable);
+ }
+ String footer = getFooter();
+ if (footer != null) {
+ body.append(footer);
+ }
+ return body.toString();
+ }
+
+ /**
+ * Gets the header to the email body.
+ */
+ protected abstract String getHeader();
+
+ /**
+ * Gets the table of entries in the email body.
+ * @param emailAddress the email address to notify
+ * @return the HTML string representing the table for the resources to send to the
+ * email address
+ */
+ protected abstract String getEntryTable(String emailAddress);
+
+ /**
+ * Gets the footer of the email body.
+ */
+ protected abstract String getFooter();
+
+ /**
+ * Gets the HTML cell in the table of a string value.
+ * @param value the string to put in the table
+ * @return the HTML text
+ */
+ protected String getHtmlCell(String value) {
+ return "
" + value + "
";
+ }
+
+ /**
+ * Gets the HTML string displaying the table header with the specified column names.
+ * @param columns the column names for the table
+ */
+ protected String getHtmlTableHeader(String[] columns) {
+ StringBuilder tableHeader = new StringBuilder();
+ tableHeader.append(
+ "
");
+ tableHeader.append("
");
+ for (String col : columns) {
+ tableHeader.append(getHtmlCell(col));
+ }
+ tableHeader.append("
");
+ return tableHeader.toString();
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/CloudClient.java b/src/main/java/com/netflix/simianarmy/CloudClient.java
index ac9b2f95..c651a968 100644
--- a/src/main/java/com/netflix/simianarmy/CloudClient.java
+++ b/src/main/java/com/netflix/simianarmy/CloudClient.java
@@ -17,6 +17,9 @@
*/
package com.netflix.simianarmy;
+import java.util.Map;
+
+
/**
* The CloudClient interface. This abstractions provides the interface that the monkeys need to interact with
* "the cloud".
@@ -24,7 +27,7 @@
public interface CloudClient {
/**
- * Terminate instance.
+ * Terminates instance.
*
* @param instanceId
* the instance id
@@ -34,4 +37,40 @@ public interface CloudClient {
* should get a NotFoundException
*/
void terminateInstance(String instanceId);
+
+ /**
+ * Deletes an auto scaling group.
+ *
+ * @param asgName
+ * the auto scaling group name
+ */
+ void deleteAutoScalingGroup(String asgName);
+
+ /**
+ * Deletes a volume.
+ *
+ * @param volumeId
+ * the volume id
+ */
+ void deleteVolume(String volumeId);
+
+ /**
+ * Deletes a snapshot.
+ *
+ * @param snapshotId
+ * the snapshot id.
+ */
+ void deleteSnapshot(String snapshotId);
+
+ /**
+ * Adds or overwrites tags for the specified resources.
+ *
+ * @param keyValueMap
+ * the new tags in the form of map from key to value
+ *
+ * @param resourceIds
+ * the list of resource ids
+ */
+ void createTagsForResources(Map keyValueMap, String... resourceIds);
+
}
diff --git a/src/main/java/com/netflix/simianarmy/EmailBuilder.java b/src/main/java/com/netflix/simianarmy/EmailBuilder.java
new file mode 100644
index 00000000..4a753031
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/EmailBuilder.java
@@ -0,0 +1,28 @@
+/*
+ *
+ * Copyright 2012 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;
+
+/** Interface for build the email body. */
+public interface EmailBuilder {
+ /**
+ * Builds an email body for an email address.
+ * @param emailAddress the email address to send notification to
+ * @return the email body
+ */
+ String buildEmailBody(String emailAddress);
+}
diff --git a/src/main/java/com/netflix/simianarmy/Monkey.java b/src/main/java/com/netflix/simianarmy/Monkey.java
index e9fef2b0..b06e00fa 100644
--- a/src/main/java/com/netflix/simianarmy/Monkey.java
+++ b/src/main/java/com/netflix/simianarmy/Monkey.java
@@ -82,10 +82,16 @@ public interface Context {
*/
String getEventReport();
+ /**
+ * Configuration.
+ *
+ * @return the monkey configuration
+ */
+ MonkeyConfiguration configuration();
}
/** The context. */
- private Context ctx;
+ private final Context ctx;
/**
* Instantiates a new monkey.
@@ -127,7 +133,10 @@ public void run() {
try {
this.doMonkeyBusiness();
} finally {
- LOGGER.info("Reporting what I did...\n" + context().getEventReport());
+ String eventReport = context().getEventReport();
+ if (eventReport != null) {
+ LOGGER.info("Reporting what I did...\n" + eventReport);
+ }
}
} else {
LOGGER.info("Not Time for " + this.type().name() + " Monkey");
@@ -140,6 +149,7 @@ public void run() {
public void start() {
final Monkey me = this;
ctx.scheduler().start(this, new Runnable() {
+ @Override
public void run() {
try {
me.run();
diff --git a/src/main/java/com/netflix/simianarmy/MonkeyCalendar.java b/src/main/java/com/netflix/simianarmy/MonkeyCalendar.java
index fe89f2e9..061212f7 100644
--- a/src/main/java/com/netflix/simianarmy/MonkeyCalendar.java
+++ b/src/main/java/com/netflix/simianarmy/MonkeyCalendar.java
@@ -18,6 +18,7 @@
package com.netflix.simianarmy;
import java.util.Calendar;
+import java.util.Date;
/**
* The Interface MonkeyCalendar used to tell if a monkey should be running or now. We only want monkeys to run during
@@ -54,4 +55,12 @@ public interface MonkeyCalendar {
* @return the calendar
*/
Calendar now();
+
+ /** Gets the next business day from the start date after n business days.
+ *
+ * @param date the start date
+ * @param n the number of business days from now
+ * @return the business day after n business days
+ */
+ Date getBusinessDay(Date date, int n);
}
diff --git a/src/main/java/com/netflix/simianarmy/MonkeyEmailNotifier.java b/src/main/java/com/netflix/simianarmy/MonkeyEmailNotifier.java
index b633d88e..e837e81e 100644
--- a/src/main/java/com/netflix/simianarmy/MonkeyEmailNotifier.java
+++ b/src/main/java/com/netflix/simianarmy/MonkeyEmailNotifier.java
@@ -20,6 +20,7 @@
/** The interface for the email notifier used by monkeys. */
public interface MonkeyEmailNotifier {
+
/**
* Determines if a email address is valid.
* @param email the email
diff --git a/src/main/java/com/netflix/simianarmy/MonkeyRecorder.java b/src/main/java/com/netflix/simianarmy/MonkeyRecorder.java
index 14fdb0e7..bcab334e 100644
--- a/src/main/java/com/netflix/simianarmy/MonkeyRecorder.java
+++ b/src/main/java/com/netflix/simianarmy/MonkeyRecorder.java
@@ -79,7 +79,7 @@ public interface Event {
*
* @param name
* the name
- * @return the string assiciated with that field
+ * @return the string associated with that field
*/
String field(String name);
diff --git a/src/main/java/com/netflix/simianarmy/MonkeyRunner.java b/src/main/java/com/netflix/simianarmy/MonkeyRunner.java
index a534c5a2..548c386c 100644
--- a/src/main/java/com/netflix/simianarmy/MonkeyRunner.java
+++ b/src/main/java/com/netflix/simianarmy/MonkeyRunner.java
@@ -72,11 +72,11 @@ public void stop() {
* The monkey map. Maps the monkey class to the context class that is registered. This is so we can create new
* monkeys in factory() that have the same context types as the registered ones.
*/
- // SUPPRESS CHECKSTYLE LineLength
- private Map, Class extends Monkey.Context>> monkeyMap = new HashMap, Class extends Monkey.Context>>();
+ private final Map, Class extends Monkey.Context>> monkeyMap =
+ new HashMap, Class extends Monkey.Context>>();
/** The monkeys. */
- private List monkeys = new LinkedList();
+ private final List monkeys = new LinkedList();
/**
* Gets the registered monkeys.
@@ -171,12 +171,11 @@ public void removeMonkey(Class extends Monkey> monkeyClass) {
* Example:
*
*
- * {@code
- * MonkeyRunner.getInstance().addMonkey(BasicChaosMonkey.class, BasicMonkeyContext.class);
- *
- * // This will actualy return a BasicChaosMonkey since that is the only subclass that was registered
- * ChaosMonkey monkey = MonkeyRunner.getInstance().factory(ChaosMonkey.class);
- * }
+ * {@code
+ * MonkeyRunner.getInstance().addMonkey(BasicChaosMonkey.class, BasicMonkeyContext.class);
+ * // This will actually return a BasicChaosMonkey since that is the only subclass that was registered
+ * ChaosMonkey monkey = MonkeyRunner.getInstance().factory(ChaosMonkey.class);
+ *}
*
*
* @param
@@ -240,7 +239,7 @@ public T factory(Class monkeyClass, Class extends Monkey
}
/**
- * Gets the context class. You shouldnt need this.
+ * Gets the context class. You should not need this.
*
* @param monkeyClass
* the monkey class
diff --git a/src/main/java/com/netflix/simianarmy/Resource.java b/src/main/java/com/netflix/simianarmy/Resource.java
new file mode 100644
index 00000000..91d8493f
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/Resource.java
@@ -0,0 +1,385 @@
+/*
+ *
+ * Copyright 2012 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;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * The interface of Resource. It defines the interfaces for getting the common properties of a resource, as well as
+ * the methods to add and retrieve the additional properties of a resource. Instead of defining a new subclass of
+ * the Resource interface, new resources that have additional fields other than the common ones can be represented,
+ * by adding field-value pairs. This approach makes serialization and deserialization of resources much easier with
+ * the cost of type safety.
+ */
+public interface Resource {
+ /** The enum representing the cleanup state of a resource. **/
+ public enum CleanupState {
+ /** The resource is marked as a cleanup candidate but has not been cleaned up yet. **/
+ MARKED,
+ /** The resource is terminated by janitor monkey. **/
+ JANITOR_TERMINATED,
+ /** The resource is terminated by user before janitor monkey performs the termination. **/
+ USER_TERMINATED,
+ /** The resource is unmarked and not for cleanup anymore due to some change of situations. **/
+ UNMARKED
+ }
+
+ /**
+ * Gets the resource id.
+ *
+ * @return the resource id
+ */
+ String getId();
+
+ /**
+ * Sets the resource id.
+ *
+ * @param id the resource id
+ */
+ void setId(String id);
+
+ /**
+ * Sets the resource id and returns the resource.
+ *
+ * @param id the resource id
+ * @return the resource object
+ */
+ Resource withId(String id);
+
+ /**
+ * Gets the resource type.
+ *
+ * @return the resource type enum
+ */
+ Enum getResourceType();
+
+ /**
+ * Sets the resource type.
+ *
+ * @param type the resource type enum
+ */
+ void setResourceType(Enum type);
+
+ /**
+ * Sets the resource type and returns the resource.
+ *
+ * @param type resource type enum
+ * @return the resource object
+ */
+ Resource withResourceType(Enum type);
+
+ /**
+ * Gets the region the resource is in.
+ *
+ * @return the region of the resource
+ */
+ String getRegion();
+
+ /**
+ * Sets the region the resource is in.
+ *
+ * @param region the region the resource is in
+ */
+ void setRegion(String region);
+
+ /**
+ * Sets the resource region and returns the resource.
+ *
+ * @param region the region the resource is in
+ * @return the resource object
+ */
+ Resource withRegion(String region);
+
+ /**
+ * Gets the owner email of the resource.
+ *
+ * @return the owner email of the resource
+ */
+ String getOwnerEmail();
+
+ /**
+ * Sets the owner email of the resource.
+ *
+ * @param ownerEmail the owner email of the resource
+ */
+ void setOwnerEmail(String ownerEmail);
+
+ /**
+ * Sets the resource owner email and returns the resource.
+ *
+ * @param ownerEmail the owner email of the resource
+ * @return the resource object
+ */
+ Resource withOwnerEmail(String ownerEmail);
+
+ /**
+ * Gets the description of the resource.
+ *
+ * @return the description of the resource
+ */
+ String getDescription();
+
+ /**
+ * Sets the description of the resource.
+ *
+ * @param description the description of the resource
+ */
+ void setDescription(String description);
+
+ /**
+ * Sets the resource description and returns the resource.
+ *
+ * @param description the description of the resource
+ * @return the resource object
+ */
+ Resource withDescription(String description);
+
+ /**
+ * Gets the launch time of the resource.
+ *
+ * @return the launch time of the resource
+ */
+ Date getLaunchTime();
+
+ /**
+ * Sets the launch time of the resource.
+ *
+ * @param launchTime the launch time of the resource
+ */
+ void setLaunchTime(Date launchTime);
+
+ /**
+ * Sets the resource launch time and returns the resource.
+ *
+ * @param launchTime the launch time of the resource
+ * @return the resource object
+ */
+ Resource withLaunchTime(Date launchTime);
+
+ /**
+ * Gets the time that when the resource is marked as a cleanup candidate.
+ *
+ * @return the time that when the resource is marked as a cleanup candidate
+ */
+ Date getMarkTime();
+
+ /**
+ * Sets the time that when the resource is marked as a cleanup candidate.
+ *
+ * @param markTime the time that when the resource is marked as a cleanup candidate
+ */
+ void setMarkTime(Date markTime);
+
+ /**
+ * Sets the resource mark time and returns the resource.
+ *
+ * @param markTime the time that when the resource is marked as a cleanup candidate
+ * @return the resource object
+ */
+ Resource withMarkTime(Date markTime);
+
+ /**
+ * Gets the the time that when the resource is expected to be terminated.
+ *
+ * @return the time that when the resource is expected to be terminated
+ */
+ Date getExpectedTerminationTime();
+
+ /**
+ * Sets the time that when the resource is expected to be terminated.
+ *
+ * @param expectedTerminationTime the time that when the resource is expected to be terminated
+ */
+ void setExpectedTerminationTime(Date expectedTerminationTime);
+
+ /**
+ * Sets the time that when the resource is expected to be terminated and returns the resource.
+ *
+ * @param expectedTerminationTime the time that when the resource is expected to be terminated
+ * @return the resource object
+ */
+ Resource withExpectedTerminationTime(Date expectedTerminationTime);
+
+ /**
+ * Gets the time that when the resource is actually terminated.
+ *
+ * @return the time that when the resource is actually terminated
+ */
+ Date getActualTerminationTime();
+
+ /**
+ * Sets the time that when the resource is actually terminated.
+ *
+ * @param actualTerminationTime the time that when the resource is actually terminated
+ */
+ void setActualTerminationTime(Date actualTerminationTime);
+
+ /**
+ * Sets the resource actual termination time and returns the resource.
+ *
+ * @param actualTerminationTime the time that when the resource is actually terminated
+ * @return the resource object
+ */
+ Resource withActualTerminationTime(Date actualTerminationTime);
+
+ /**
+ * Gets the time that when the owner is notified about the cleanup of the resource.
+ *
+ * @return the time that when the owner is notified about the cleanup of the resource
+ */
+ Date getNotificationTime();
+
+ /**
+ * Sets the time that when the owner is notified about the cleanup of the resource.
+ *
+ * @param notificationTime the time that when the owner is notified about the cleanup of the resource
+ */
+ void setNotificationTime(Date notificationTime);
+
+ /**
+ * Sets the time that when the owner is notified about the cleanup of the resource and returns the resource.
+ *
+ * @param notificationTime the time that when the owner is notified about the cleanup of the resource
+ * @return the resource object
+ */
+ Resource withNnotificationTime(Date notificationTime);
+
+ /**
+ * Gets the resource state.
+ *
+ * @return the resource state enum
+ */
+ CleanupState getState();
+
+ /**
+ * Sets the resource state.
+ *
+ * @param state the resource state
+ */
+ void setState(CleanupState state);
+
+ /**
+ * Sets the resource state and returns the resource.
+ *
+ * @param state resource state enum
+ * @return the resource object
+ */
+ Resource withState(CleanupState state);
+
+ /**
+ * Gets the termination reason of the resource.
+ *
+ * @return the termination reason of the resource
+ */
+ String getTerminationReason();
+
+ /**
+ * Sets the termination reason of the resource.
+ *
+ * @param terminationReason the termination reason of the resource
+ */
+ void setTerminationReason(String terminationReason);
+
+ /**
+ * Sets the resource termination reason and returns the resource.
+ *
+ * @param terminationReason the termination reason of the resource
+ * @return the resource object
+ */
+ Resource withTerminationReason(String terminationReason);
+
+ /**
+ * Gets the boolean to indicate whether or not the resource is opted out of Janitor monkey
+ * so it will not be cleaned.
+ * @return true if the resource is opted out of Janitor monkey, otherwise false
+ */
+ boolean isOptOutOfJanitor();
+
+ /**
+ * Sets the flag to indicate whether or not the resource is opted out of Janitor monkey
+ * so it will not be cleaned.
+ * @param optOutOfJanitor true if the resource is opted out of Janitor monkey, otherwise false
+ */
+ void setOptOutOfJanitor(boolean optOutOfJanitor);
+
+ /**
+ * Sets the flag to indicate whether or not the resource is opted out of Janitor monkey
+ * so it will not be cleaned and returns the resource object.
+ * @param optOutOfJanitor true if the resource is opted out of Janitor monkey, otherwise false
+ * @return the resource object
+ */
+ Resource withOptOutOfJanitor(boolean optOutOfJanitor);
+
+ /**
+ * Gets a map from fields of resources to corresponding values. Values are represented
+ * as Strings so they can be displayed or stored in databases like SimpleDB.
+ * @return a map from field name to field value
+ */
+ Map getFieldToValueMap();
+
+ /** Adds or sets an additional field with the specified name and value to the resource.
+ *
+ * @param fieldName the field name
+ * @param fieldValue the field value
+ * @return the resource itself for chaining
+ */
+ Resource setAdditionalField(String fieldName, String fieldValue);
+
+ /** Gets the value of an additional field with the specified name of the resource.
+ *
+ * @param fieldName the field name
+ * @return the field value
+ */
+ String getAdditionalField(String fieldName);
+
+ /**
+ * Gets all additional field names in the resource.
+ * @return a collection of names of all additional fields
+ */
+ Collection getAdditionalFieldNames();
+
+ /**
+ * Adds a tag with the specified key and value to the resource.
+ * @param key the key of the tag
+ * @param value the value of the tag
+ */
+ void setTag(String key, String value);
+
+ /**
+ * Gets the tag value for a specific key of the resource.
+ * @param key the key of the tag
+ * @return the value of the tag
+ */
+ String getTag(String key);
+
+ /**
+ * Gets all the keys of tags.
+ * @return collection of keys of all tags
+ */
+ Collection getAllTagKeys();
+
+
+ /** Clone a resource with the exact field values of the current object.
+ *
+ * @return the clone of the resource
+ */
+ Resource cloneResource();
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/AWSEmailNotifier.java b/src/main/java/com/netflix/simianarmy/aws/AWSEmailNotifier.java
index 67e7fefa..2adfb86f 100644
--- a/src/main/java/com/netflix/simianarmy/aws/AWSEmailNotifier.java
+++ b/src/main/java/com/netflix/simianarmy/aws/AWSEmailNotifier.java
@@ -34,7 +34,7 @@
/**
* The class implements the monkey email notifier using AWS simple email service
- * for sending emails.
+ * for sending email.
*/
public abstract class AWSEmailNotifier implements MonkeyEmailNotifier {
/** The Constant LOGGER. */
diff --git a/src/main/java/com/netflix/simianarmy/aws/AWSResource.java b/src/main/java/com/netflix/simianarmy/aws/AWSResource.java
new file mode 100644
index 00000000..604e1e84
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/AWSResource.java
@@ -0,0 +1,512 @@
+/*
+ *
+ * Copyright 2012 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;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.Validate;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+import com.netflix.simianarmy.Resource;
+
+/**
+ * The class represents general AWS resources that are managed by janitor monkey.
+ */
+public class AWSResource implements Resource {
+ private String id;
+ private Enum resourceType;
+ private String region;
+ private String ownerEmail;
+ private String description;
+ private String terminationReason;
+ private CleanupState state;
+ private Date expectedTerminationTime;
+ private Date actualTerminationTime;
+ private Date notificationTime;
+ private Date launchTime;
+ private Date markTime;
+ private boolean optOutOfJanitor;
+ private String awsResourceState;
+
+ /** The field name for resourceId. **/
+ public static final String FIELD_RESOURCE_ID = "resourceId";
+ /** The field name for resourceType. **/
+ public static final String FIELD_RESOURCE_TYPE = "resourceType";
+ /** The field name for region. **/
+ public static final String FIELD_REGION = "region";
+ /** The field name for owner email. **/
+ public static final String FIELD_OWNER_EMAIL = "ownerEmail";
+ /** The field name for description. **/
+ public static final String FIELD_DESCRIPTION = "description";
+ /** The field name for state. **/
+ public static final String FIELD_STATE = "state";
+ /** The field name for terminationReason. **/
+ public static final String FIELD_TERMINATION_REASON = "terminationReason";
+ /** The field name for expectedTerminationTime. **/
+ public static final String FIELD_EXPECTED_TERMINATION_TIME = "expectedTerminationTime";
+ /** The field name for actualTerminationTime. **/
+ public static final String FIELD_ACTUAL_TERMINATION_TIME = "actualTerminationTime";
+ /** The field name for notificationTime. **/
+ public static final String FIELD_NOTIFICATION_TIME = "notificationTime";
+ /** The field name for launchTime. **/
+ public static final String FIELD_LAUNCH_TIME = "launchTime";
+ /** The field name for markTime. **/
+ public static final String FIELD_MARK_TIME = "markTime";
+ /** The field name for isOptOutOfJanitor. **/
+ public static final String FIELD_OPT_OUT_OF_JANITOR = "optOutOfJanitor";
+ /** The field name for awsResourceState. **/
+ public static final String FIELD_AWS_RESOURCE_STATE = "awsResourceState";
+
+ /** The date format used to print or parse a Date value. **/
+ public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss");
+
+ /** The map from name to value for additional fields used by the resource. **/
+ private final Map additionalFields = new HashMap();
+
+ /** The map from AWS tag key to value for the resource. **/
+ private final Map tags = new HashMap();
+
+ /** {@inheritDoc} */
+ @Override
+ public Map getFieldToValueMap() {
+ Map fieldToValue = new HashMap();
+
+ putToMapIfNotNull(fieldToValue, FIELD_RESOURCE_ID, getId());
+ putToMapIfNotNull(fieldToValue, FIELD_RESOURCE_TYPE, getResourceType());
+ putToMapIfNotNull(fieldToValue, FIELD_REGION, getRegion());
+ putToMapIfNotNull(fieldToValue, FIELD_OWNER_EMAIL, getOwnerEmail());
+ putToMapIfNotNull(fieldToValue, FIELD_DESCRIPTION, getDescription());
+ putToMapIfNotNull(fieldToValue, FIELD_STATE, getState());
+ putToMapIfNotNull(fieldToValue, FIELD_TERMINATION_REASON, getTerminationReason());
+ putToMapIfNotNull(fieldToValue, FIELD_EXPECTED_TERMINATION_TIME, printDate(getExpectedTerminationTime()));
+ putToMapIfNotNull(fieldToValue, FIELD_ACTUAL_TERMINATION_TIME, printDate(getActualTerminationTime()));
+ putToMapIfNotNull(fieldToValue, FIELD_NOTIFICATION_TIME, printDate(getNotificationTime()));
+ putToMapIfNotNull(fieldToValue, FIELD_LAUNCH_TIME, printDate(getLaunchTime()));
+ putToMapIfNotNull(fieldToValue, FIELD_MARK_TIME, printDate(getMarkTime()));
+ putToMapIfNotNull(fieldToValue, FIELD_AWS_RESOURCE_STATE, getAWSResourceState());
+
+ // Additional fields are serialized while tags are not. So if any tags need to be
+ // serialized as well, put them to additional fields.
+ fieldToValue.put(FIELD_OPT_OUT_OF_JANITOR, String.valueOf(isOptOutOfJanitor()));
+
+ fieldToValue.putAll(additionalFields);
+
+ return fieldToValue;
+ }
+
+ /**
+ * Parse a map from field name to value to a resource.
+ * @param fieldToValue the map from field name to value
+ * @return the resource that is de-serialized from the map
+ */
+ public static AWSResource parseFieldtoValueMap(Map fieldToValue) {
+ AWSResource resource = new AWSResource();
+ for (Map.Entry field : fieldToValue.entrySet()) {
+ String name = field.getKey();
+ String value = field.getValue();
+ if (name.equals(FIELD_RESOURCE_ID)) {
+ resource.setId(value);
+ } else if (name.equals(FIELD_RESOURCE_TYPE)) {
+ resource.setResourceType(AWSResourceType.valueOf(value));
+ } else if (name.equals(FIELD_REGION)) {
+ resource.setRegion(value);
+ } else if (name.equals(FIELD_OWNER_EMAIL)) {
+ resource.setOwnerEmail(value);
+ } else if (name.equals(FIELD_DESCRIPTION)) {
+ resource.setDescription(value);
+ } else if (name.equals(FIELD_STATE)) {
+ resource.setState(CleanupState.valueOf(value));
+ } else if (name.equals(FIELD_TERMINATION_REASON)) {
+ resource.setTerminationReason(value);
+ } else if (name.equals(FIELD_EXPECTED_TERMINATION_TIME)) {
+ resource.setExpectedTerminationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
+ } else if (name.equals(FIELD_ACTUAL_TERMINATION_TIME)) {
+ resource.setActualTerminationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
+ } else if (name.equals(FIELD_NOTIFICATION_TIME)) {
+ resource.setNotificationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
+ } else if (name.equals(FIELD_LAUNCH_TIME)) {
+ resource.setLaunchTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
+ } else if (name.equals(FIELD_MARK_TIME)) {
+ resource.setMarkTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));
+ } else if (name.equals(FIELD_AWS_RESOURCE_STATE)) {
+ resource.setAWSResourceState(value);
+ } else if (name.equals(FIELD_OPT_OUT_OF_JANITOR)) {
+ resource.setOptOutOfJanitor("true".equals(value));
+ } else {
+ // put all other fields into additional fields
+ resource.setAdditionalField(name, value);
+ }
+ }
+ return resource;
+ }
+
+ public String getAWSResourceState() {
+ return awsResourceState;
+ }
+
+ public void setAWSResourceState(String awsState) {
+ this.awsResourceState = awsState;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withId(String resourceId) {
+ setId(resourceId);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Enum getResourceType() {
+ return resourceType;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setResourceType(Enum resourceType) {
+ this.resourceType = resourceType;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withResourceType(Enum type) {
+ setResourceType(type);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getRegion() {
+ return region;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withRegion(String resourceRegion) {
+ setRegion(resourceRegion);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getOwnerEmail() {
+ return ownerEmail;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setOwnerEmail(String ownerEmail) {
+ this.ownerEmail = ownerEmail;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withOwnerEmail(String resourceOwner) {
+ setOwnerEmail(resourceOwner);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withDescription(String resourceDescription) {
+ setDescription(resourceDescription);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Date getLaunchTime() {
+ return getCopyOfDate(launchTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setLaunchTime(Date launchTime) {
+ this.launchTime = getCopyOfDate(launchTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withLaunchTime(Date resourceLaunchTime) {
+ setLaunchTime(resourceLaunchTime);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Date getMarkTime() {
+ return getCopyOfDate(markTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setMarkTime(Date markTime) {
+ this.markTime = getCopyOfDate(markTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withMarkTime(Date resourceMarkTime) {
+ setMarkTime(resourceMarkTime);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Date getExpectedTerminationTime() {
+ return getCopyOfDate(expectedTerminationTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setExpectedTerminationTime(Date expectedTerminationTime) {
+ this.expectedTerminationTime = getCopyOfDate(expectedTerminationTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withExpectedTerminationTime(Date resourceExpectedTerminationTime) {
+ setExpectedTerminationTime(resourceExpectedTerminationTime);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Date getActualTerminationTime() {
+ return getCopyOfDate(actualTerminationTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setActualTerminationTime(Date actualTerminationTime) {
+ this.actualTerminationTime = getCopyOfDate(actualTerminationTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withActualTerminationTime(Date resourceActualTerminationTime) {
+ setActualTerminationTime(resourceActualTerminationTime);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Date getNotificationTime() {
+ return getCopyOfDate(notificationTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setNotificationTime(Date notificationTime) {
+ this.notificationTime = getCopyOfDate(notificationTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withNnotificationTime(Date resourceNotificationTime) {
+ setNotificationTime(resourceNotificationTime);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CleanupState getState() {
+ return state;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setState(CleanupState state) {
+ this.state = state;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withState(CleanupState resourceState) {
+ setState(resourceState);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getTerminationReason() {
+ return terminationReason;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setTerminationReason(String terminationReason) {
+ this.terminationReason = terminationReason;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withTerminationReason(String resourceTerminationReason) {
+ setTerminationReason(resourceTerminationReason);
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isOptOutOfJanitor() {
+ return optOutOfJanitor;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setOptOutOfJanitor(boolean optOutOfJanitor) {
+ this.optOutOfJanitor = optOutOfJanitor;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Resource withOptOutOfJanitor(boolean optOut) {
+ setOptOutOfJanitor(optOut);
+ return this;
+ }
+
+ private static Date getCopyOfDate(Date date) {
+ if (date == null) {
+ return null;
+ }
+ return new Date(date.getTime());
+ }
+
+ private static void putToMapIfNotNull(Map map, String key, String value) {
+ Validate.notNull(map);
+ Validate.notNull(key);
+ if (value != null) {
+ map.put(key, value);
+ }
+ }
+
+ private static void putToMapIfNotNull(Map map, String key, Enum value) {
+ Validate.notNull(map);
+ Validate.notNull(key);
+ if (value != null) {
+ map.put(key, value.name());
+ }
+ }
+
+ private static String printDate(Date date) {
+ if (date == null) {
+ return null;
+ }
+
+ return DATE_FORMATTER.print(date.getTime());
+ }
+
+ @Override
+ public Resource setAdditionalField(String fieldName, String fieldValue) {
+ Validate.notNull(fieldName);
+ Validate.notNull(fieldValue);
+ putToMapIfNotNull(additionalFields, fieldName, fieldValue);
+ return this;
+ }
+
+ @Override
+ public String getAdditionalField(String fieldName) {
+ return additionalFields.get(fieldName);
+ }
+
+ @Override
+ public Collection getAdditionalFieldNames() {
+ return additionalFields.keySet();
+ }
+
+ @Override
+ public Resource cloneResource() {
+ Resource clone = new AWSResource()
+ .withActualTerminationTime(getActualTerminationTime())
+ .withDescription(getDescription())
+ .withExpectedTerminationTime(getExpectedTerminationTime())
+ .withId(getId())
+ .withLaunchTime(getLaunchTime())
+ .withMarkTime(getMarkTime())
+ .withNnotificationTime(getNotificationTime())
+ .withOwnerEmail(getOwnerEmail())
+ .withRegion(getRegion())
+ .withResourceType(getResourceType())
+ .withState(getState())
+ .withTerminationReason(getTerminationReason())
+ .withOptOutOfJanitor(isOptOutOfJanitor());
+ ((AWSResource) clone).setAWSResourceState(awsResourceState);
+
+ ((AWSResource) clone).additionalFields.putAll(additionalFields);
+
+ for (String key : this.getAllTagKeys()) {
+ clone.setTag(key, this.getTag(key));
+ }
+
+ return clone;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setTag(String key, String value) {
+ tags.put(key, value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getTag(String key) {
+ return tags.get(key);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Collection getAllTagKeys() {
+ return tags.keySet();
+ }
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/AWSResourceType.java b/src/main/java/com/netflix/simianarmy/aws/AWSResourceType.java
new file mode 100644
index 00000000..e81856ac
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/AWSResourceType.java
@@ -0,0 +1,39 @@
+/*
+ *
+ * Copyright 2012 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;
+
+/**
+ * The enum of resource types of AWS.
+ */
+public enum AWSResourceType {
+ /** AWS instance. */
+ INSTANCE,
+ /** AWS EBS volume. */
+ EBS_VOLUME,
+ /** AWS EBS snapshot. */
+ EBS_SNAPSHOT,
+ /** AWS auto scaling group. */
+ ASG,
+ /** AWS launch configuration. */
+ LAUNCH_CONFIG,
+ /** AWS S3 bucket. */
+ S3_BUCKET,
+ /** AWS security group. */
+ SECURITY_GROUP
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/SimpleDBRecorder.java b/src/main/java/com/netflix/simianarmy/aws/SimpleDBRecorder.java
index 0b7efd96..a0a19d04 100644
--- a/src/main/java/com/netflix/simianarmy/aws/SimpleDBRecorder.java
+++ b/src/main/java/com/netflix/simianarmy/aws/SimpleDBRecorder.java
@@ -27,11 +27,9 @@
import java.util.Map;
import java.util.Set;
-import com.amazonaws.auth.AWSCredentials;
-import com.amazonaws.auth.BasicAWSCredentials;
-import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
+import org.apache.commons.lang.Validate;
+
import com.amazonaws.services.simpledb.AmazonSimpleDB;
-import com.amazonaws.services.simpledb.AmazonSimpleDBClient;
import com.amazonaws.services.simpledb.model.Attribute;
import com.amazonaws.services.simpledb.model.Item;
import com.amazonaws.services.simpledb.model.PutAttributesRequest;
@@ -40,6 +38,7 @@
import com.amazonaws.services.simpledb.model.SelectResult;
import com.netflix.simianarmy.MonkeyRecorder;
import com.netflix.simianarmy.basic.BasicRecorderEvent;
+import com.netflix.simianarmy.client.aws.AWSClient;
/**
* The Class SimpleDBRecorder. Records events to and fetched events from a Amazon SimpleDB table (default SIMIAN_ARMY)
@@ -47,14 +46,12 @@
@SuppressWarnings("serial")
public class SimpleDBRecorder implements MonkeyRecorder {
- /** The cred. */
- private AWSCredentials cred;
+ private final AmazonSimpleDB simpleDBClient;
- /** The region. */
- private String region;
+ private final String region;
/** The domain. */
- private String domain;
+ private final String domain;
/**
* The Enum Keys.
@@ -87,65 +84,26 @@ private enum Keys {
/**
* Instantiates a new simple db recorder.
*
- * @param accessKey
- * the access key
- * @param secretKey
- * the secret key
- * @param region
- * the region
- * @param domain
- * the domain
- */
- public SimpleDBRecorder(String accessKey, String secretKey, String region, String domain) {
- this.cred = new BasicAWSCredentials(accessKey, secretKey);
- this.region = region;
- this.domain = domain;
- }
-
- /**
- * Instantiates a new simple db recorder.
- *
- * @param cred
- * the cred
- * @param region
- * the region
+ * @param awsClient
+ * the AWS client
* @param domain
* the domain
*/
- public SimpleDBRecorder(AWSCredentials cred, String region, String domain) {
- this.cred = cred;
- this.region = region;
+ public SimpleDBRecorder(AWSClient awsClient, String domain) {
+ Validate.notNull(awsClient);
+ Validate.notNull(domain);
+ this.simpleDBClient = awsClient.sdbClient();
+ this.region = awsClient.region();
this.domain = domain;
}
- /**
- * Use {@link DefaultAWSCredentialsProviderChain} to provide credentials.
- *
- * @param region
- * the region
- * @param domain
- * the domain
- */
- public SimpleDBRecorder(String region, String domain) {
- this(new DefaultAWSCredentialsProviderChain().getCredentials(), region, domain);
- }
-
/**
* simple client. abstracted to aid testing
*
* @return the amazon simple db
*/
protected AmazonSimpleDB sdbClient() {
- AmazonSimpleDB client = new AmazonSimpleDBClient(cred);
-
- // us-east-1 has special naming
- // http://docs.amazonwebservices.com/general/latest/gr/rande.html#sdb_region
- if (region.equals("us-east-1")) {
- client.setEndpoint("sdb.amazonaws.com");
- } else {
- client.setEndpoint("sdb." + region + ".amazonaws.com");
- }
- return client;
+ return simpleDBClient;
}
/**
@@ -188,15 +146,18 @@ private static Enum valueToEnum(String value) {
}
/** {@inheritDoc} */
+ @Override
public Event newEvent(Enum monkeyType, Enum eventType, String reg, String id) {
return new BasicRecorderEvent(monkeyType, eventType, reg, id);
}
/** {@inheritDoc} */
+ @Override
public void recordEvent(Event evt) {
+ String evtTime = String.valueOf(evt.eventTime().getTime());
List attrs = new LinkedList();
attrs.add(new ReplaceableAttribute(Keys.id.name(), evt.id(), true));
- attrs.add(new ReplaceableAttribute(Keys.eventTime.name(), String.valueOf(evt.eventTime().getTime()), true));
+ attrs.add(new ReplaceableAttribute(Keys.eventTime.name(), evtTime, true));
attrs.add(new ReplaceableAttribute(Keys.region.name(), evt.region(), true));
attrs.add(new ReplaceableAttribute(Keys.recordType.name(), "MonkeyEvent", true));
attrs.add(new ReplaceableAttribute(Keys.monkeyType.name(), enumToValue(evt.monkeyType()), true));
@@ -207,7 +168,8 @@ public void recordEvent(Event evt) {
}
attrs.add(new ReplaceableAttribute(pair.getKey(), pair.getValue(), true));
}
- String pk = String.format("%s-%s-%s", evt.monkeyType().name(), evt.id(), region);
+ // Let pk contain the timestamp so that the same resource can have multiple events.
+ String pk = String.format("%s-%s-%s-%s", evt.monkeyType().name(), evt.id(), region, evtTime);
PutAttributesRequest putReq = new PutAttributesRequest(domain, pk, attrs);
sdbClient().putAttributes(putReq);
@@ -223,7 +185,8 @@ public void recordEvent(Event evt) {
* @return the list
*/
protected List findEvents(Map queryMap, long after) {
- StringBuilder query = new StringBuilder(String.format("select * from %s where region = '%s'", domain, region));
+ StringBuilder query = new StringBuilder(
+ String.format("select * from %s where region = '%s'", domain, region));
for (Map.Entry pair : queryMap.entrySet()) {
query.append(String.format(" and %s = '%s'", pair.getKey(), pair.getValue()));
}
@@ -260,11 +223,13 @@ protected List findEvents(Map queryMap, long after) {
}
/** {@inheritDoc} */
+ @Override
public List findEvents(Map query, Date after) {
return findEvents(query, after.getTime());
}
/** {@inheritDoc} */
+ @Override
public List findEvents(Enum monkeyType, Map query, Date after) {
Map copy = new LinkedHashMap(query);
copy.put(Keys.monkeyType.name(), enumToValue(monkeyType));
@@ -272,6 +237,7 @@ public List findEvents(Enum monkeyType, Map query, Date a
}
/** {@inheritDoc} */
+ @Override
public List findEvents(Enum monkeyType, Enum eventType, Map query, Date after) {
Map copy = new LinkedHashMap(query);
copy.put(Keys.monkeyType.name(), enumToValue(monkeyType));
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/ASGJanitor.java b/src/main/java/com/netflix/simianarmy/aws/janitor/ASGJanitor.java
new file mode 100644
index 00000000..a925fdac
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/ASGJanitor.java
@@ -0,0 +1,64 @@
+/*
+ *
+ * Copyright 2012 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.janitor;
+
+import org.apache.commons.lang.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResourceType;
+import com.netflix.simianarmy.client.aws.AWSClient;
+import com.netflix.simianarmy.janitor.AbstractJanitor;
+
+/**
+ * The Janitor responsible for ASG cleanup.
+ */
+public class ASGJanitor extends AbstractJanitor {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJanitor.class);
+
+ private final AWSClient awsClient;
+
+ /**
+ * Constructor.
+ * @param awsClient the AWS client
+ * @param ctx the context
+ */
+ public ASGJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {
+ super(ctx, AWSResourceType.ASG);
+ Validate.notNull(awsClient);
+ this.awsClient = awsClient;
+ }
+
+ @Override
+ protected void postMark(Resource resource) {
+ }
+
+ @Override
+ protected void cleanup(Resource resource) {
+ LOGGER.info(String.format("Deleting ASG %s", resource.getId()));
+ awsClient.deleteAutoScalingGroup(resource.getId());
+ }
+
+ @Override
+ protected void postCleanup(Resource resource) {
+ }
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/EBSSnapshotJanitor.java b/src/main/java/com/netflix/simianarmy/aws/janitor/EBSSnapshotJanitor.java
new file mode 100644
index 00000000..e6309db3
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/EBSSnapshotJanitor.java
@@ -0,0 +1,64 @@
+/*
+ *
+ * Copyright 2012 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.janitor;
+
+import org.apache.commons.lang.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResourceType;
+import com.netflix.simianarmy.client.aws.AWSClient;
+import com.netflix.simianarmy.janitor.AbstractJanitor;
+
+/**
+ * The Janitor responsible for EBS snapshot cleanup.
+ */
+public class EBSSnapshotJanitor extends AbstractJanitor {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(EBSSnapshotJanitor.class);
+
+ private final AWSClient awsClient;
+
+ /**
+ * Constructor.
+ * @param awsClient the AWS client
+ * @param ctx the context
+ */
+ public EBSSnapshotJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {
+ super(ctx, AWSResourceType.EBS_SNAPSHOT);
+ Validate.notNull(awsClient);
+ this.awsClient = awsClient;
+ }
+
+ @Override
+ protected void postMark(Resource resource) {
+ }
+
+ @Override
+ protected void cleanup(Resource resource) {
+ LOGGER.info(String.format("Deleting EBS snapshot %s", resource.getId()));
+ awsClient.deleteSnapshot(resource.getId());
+ }
+
+ @Override
+ protected void postCleanup(Resource resource) {
+ }
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/EBSVolumeJanitor.java b/src/main/java/com/netflix/simianarmy/aws/janitor/EBSVolumeJanitor.java
new file mode 100644
index 00000000..90effebe
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/EBSVolumeJanitor.java
@@ -0,0 +1,64 @@
+/*
+ *
+ * Copyright 2012 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.janitor;
+
+import org.apache.commons.lang.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResourceType;
+import com.netflix.simianarmy.client.aws.AWSClient;
+import com.netflix.simianarmy.janitor.AbstractJanitor;
+
+/**
+ * The Janitor responsible for EBS volume cleanup.
+ */
+public class EBSVolumeJanitor extends AbstractJanitor {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(EBSVolumeJanitor.class);
+
+ private final AWSClient awsClient;
+
+ /**
+ * Constructor.
+ * @param awsClient the AWS client
+ * @param ctx the context
+ */
+ public EBSVolumeJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {
+ super(ctx, AWSResourceType.EBS_VOLUME);
+ Validate.notNull(awsClient);
+ this.awsClient = awsClient;
+ }
+
+ @Override
+ protected void postMark(Resource resource) {
+ }
+
+ @Override
+ protected void cleanup(Resource resource) {
+ LOGGER.info(String.format("Deleting EBS volume %s", resource.getId()));
+ awsClient.deleteVolume(resource.getId());
+ }
+
+ @Override
+ protected void postCleanup(Resource resource) {
+ }
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/InstanceJanitor.java b/src/main/java/com/netflix/simianarmy/aws/janitor/InstanceJanitor.java
new file mode 100644
index 00000000..a9478c52
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/InstanceJanitor.java
@@ -0,0 +1,64 @@
+/*
+ *
+ * Copyright 2012 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.janitor;
+
+import org.apache.commons.lang.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResourceType;
+import com.netflix.simianarmy.client.aws.AWSClient;
+import com.netflix.simianarmy.janitor.AbstractJanitor;
+
+/**
+ * The Janitor responsible for auto scaling instance cleanup.
+ */
+public class InstanceJanitor extends AbstractJanitor {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(InstanceJanitor.class);
+
+ private final AWSClient awsClient;
+
+ /**
+ * Constructor.
+ * @param awsClient the AWS client
+ * @param ctx the context
+ */
+ public InstanceJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {
+ super(ctx, AWSResourceType.INSTANCE);
+ Validate.notNull(awsClient);
+ this.awsClient = awsClient;
+ }
+
+ @Override
+ protected void postMark(Resource resource) {
+ }
+
+ @Override
+ protected void cleanup(Resource resource) {
+ LOGGER.info(String.format("Terminating instance %s", resource.getId()));
+ awsClient.terminateInstance(resource.getId());
+ }
+
+ @Override
+ protected void postCleanup(Resource resource) {
+ }
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/SimpleDBJanitorResourceTracker.java b/src/main/java/com/netflix/simianarmy/aws/janitor/SimpleDBJanitorResourceTracker.java
new file mode 100644
index 00000000..51496637
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/SimpleDBJanitorResourceTracker.java
@@ -0,0 +1,195 @@
+/*
+ *
+ * Copyright 2012 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.janitor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.amazonaws.services.simpledb.AmazonSimpleDB;
+import com.amazonaws.services.simpledb.model.Attribute;
+import com.amazonaws.services.simpledb.model.Item;
+import com.amazonaws.services.simpledb.model.PutAttributesRequest;
+import com.amazonaws.services.simpledb.model.ReplaceableAttribute;
+import com.amazonaws.services.simpledb.model.SelectRequest;
+import com.amazonaws.services.simpledb.model.SelectResult;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.Resource.CleanupState;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.client.aws.AWSClient;
+import com.netflix.simianarmy.janitor.JanitorResourceTracker;
+
+/**
+ * The JanitorResourceTracker implementation in SimpleDB.
+ */
+public class SimpleDBJanitorResourceTracker implements JanitorResourceTracker {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDBJanitorResourceTracker.class);
+
+ /** The domain. */
+ private final String domain;
+
+ /** The SimpleDB client. */
+ private final AmazonSimpleDB simpleDBClient;
+
+ /**
+ * Instantiates a new simple db resource tracker.
+ *
+ * @param awsClient
+ * the AWS Client
+ * @param domain
+ * the domain
+ */
+ public SimpleDBJanitorResourceTracker(AWSClient awsClient, String domain) {
+ this.domain = domain;
+ this.simpleDBClient = awsClient.sdbClient();
+ }
+
+ /**
+ * Gets the SimpleDB client.
+ * @return the SimpleDB client
+ */
+ protected AmazonSimpleDB getSimpleDBClient() {
+ return simpleDBClient;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addOrUpdate(Resource resource) {
+ List attrs = new ArrayList();
+ Map fieldToValueMap = resource.getFieldToValueMap();
+ for (Map.Entry entry : fieldToValueMap.entrySet()) {
+ attrs.add(new ReplaceableAttribute(entry.getKey(), entry.getValue(), true));
+ }
+ PutAttributesRequest putReqest = new PutAttributesRequest(domain, getSimpleDBItemName(resource), attrs);
+ LOGGER.debug(String.format("Saving resource %s to SimpleDB domain %s",
+ resource.getId(), domain));
+ this.simpleDBClient.putAttributes(putReqest);
+ LOGGER.debug("Successfully saved.");
+ }
+
+ /**
+ * Returns a list of AWSResource objects. You need to override this method if more
+ * specific resource types (e.g. subtypes of AWSResource) need to be obtained from
+ * the SimpleDB.
+ */
+ @Override
+ public List getResources(Enum resourceType, CleanupState state, String resourceRegion) {
+ Validate.notEmpty(resourceRegion);
+ List resources = new ArrayList();
+ StringBuilder query = new StringBuilder();
+ query.append(String.format("select * from %s where ", domain));
+ if (resourceType != null) {
+ query.append(String.format("resourceType='%s' and ", resourceType));
+ }
+ if (state != null) {
+ query.append(String.format("state='%s' and ", state));
+ }
+ query.append(String.format("region='%s'", resourceRegion));
+
+ LOGGER.debug(String.format("Query is '%s'", query));
+
+ List items = querySimpleDBItems(query.toString());
+ for (Item item : items) {
+ try {
+ resources.add(parseResource(item));
+ } catch (Exception e) {
+ // Ignore the item that cannot be parsed.
+ LOGGER.error(String.format("SimpleDB item %s cannot be parsed into a resource.", item));
+ }
+ }
+ LOGGER.info(String.format("Retrieved %d resources from SimpleDB in domain %s for resource type %s"
+ + " and state %s and region %s",
+ resources.size(), domain, resourceType, state, resourceRegion));
+ return resources;
+ }
+
+ @Override
+ public Resource getResource(String resourceId) {
+ Validate.notEmpty(resourceId);
+ StringBuilder query = new StringBuilder();
+ query.append(String.format("select * from %s where resourceId = '%s'", domain, resourceId));
+
+ LOGGER.debug(String.format("Query is '%s'", query));
+
+ List items = querySimpleDBItems(query.toString());
+ Validate.isTrue(items.size() <= 1);
+ if (items.size() == 0) {
+ LOGGER.info(String.format("Not found resource with id %s", resourceId));
+ return null;
+ } else {
+ Resource resource = null;
+ try {
+ resource = parseResource(items.get(0));
+ } catch (Exception e) {
+ // Ignore the item that cannot be parsed.
+ LOGGER.error(String.format("SimpleDB item %s cannot be parsed into a resource.", items.get(0)));
+ }
+ return resource;
+ }
+ }
+
+ /**
+ * Parses a SimpleDB item into an AWS resource.
+ * @param item the item from SimpleDB
+ * @return the AWSResource object for the SimpleDB item
+ */
+ protected Resource parseResource(Item item) {
+ Map fieldToValue = new HashMap();
+ for (Attribute attr : item.getAttributes()) {
+ String name = attr.getName();
+ String value = attr.getValue();
+ if (name != null && value != null) {
+ fieldToValue.put(name, value);
+ }
+ }
+ return AWSResource.parseFieldtoValueMap(fieldToValue);
+ }
+
+ /**
+ * Gets the unique SimpleDB item name for a resource. The subclass can override this
+ * method to generate the item name differently.
+ * @param resource
+ * @return the SimpleDB item name for the resource
+ */
+ protected String getSimpleDBItemName(Resource resource) {
+ return String.format("%s-%s-%s", resource.getResourceType().name(), resource.getId(), resource.getRegion());
+ }
+
+ private List querySimpleDBItems(String query) {
+ Validate.notNull(query);
+ String nextToken = null;
+ List items = new ArrayList();
+ do {
+ SelectRequest request = new SelectRequest(query);
+ request.setNextToken(nextToken);
+ request.setConsistentRead(Boolean.TRUE);
+ SelectResult result = this.simpleDBClient.select(request);
+ items.addAll(result.getItems());
+ nextToken = result.getNextToken();
+ } while (nextToken != null);
+
+ return items;
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/VolumeTaggingMonkey.java b/src/main/java/com/netflix/simianarmy/aws/janitor/VolumeTaggingMonkey.java
new file mode 100644
index 00000000..0dc871ee
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/VolumeTaggingMonkey.java
@@ -0,0 +1,307 @@
+/*
+ *
+ * Copyright 2012 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.janitor;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.amazonaws.services.ec2.model.Instance;
+import com.amazonaws.services.ec2.model.Tag;
+import com.amazonaws.services.ec2.model.Volume;
+import com.amazonaws.services.ec2.model.VolumeAttachment;
+import com.netflix.simianarmy.Monkey;
+import com.netflix.simianarmy.MonkeyCalendar;
+import com.netflix.simianarmy.MonkeyConfiguration;
+import com.netflix.simianarmy.MonkeyRecorder.Event;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.client.aws.AWSClient;
+import com.netflix.simianarmy.janitor.JanitorMonkey;
+
+/**
+ * A companion monkey of Janitor Monkey for tagging EBS volumes with the last attachment information.
+ * In many scenarios, EBS volumes generated by applications remain unattached to instances. Amazon
+ * does not keep track of last unattached time, which makes it difficult to determine its usage.
+ * To solve this, this monkey will tag all EBS volumes with last owner and instance to which they are attached
+ * and the time they got detached from instance. The monkey will poll and monitor EBS volumes hourly (by default).
+ *
+ */
+public class VolumeTaggingMonkey extends Monkey {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(VolumeTaggingMonkey.class);
+
+ /**
+ * The Interface Context.
+ */
+ public interface Context extends Monkey.Context {
+ /**
+ * Configuration.
+ *
+ * @return the monkey configuration
+ */
+ @Override
+ MonkeyConfiguration configuration();
+
+ /**
+ * AWS client.
+ *
+ * @return the AWS client
+ */
+ AWSClient awsClient();
+ }
+
+ private final MonkeyConfiguration config;
+ private final AWSClient awsClient;
+ private final MonkeyCalendar calendar;
+
+ /** We cache the global map from instance id to its owner when starting the monkey. */
+ private final Map instanceToOwner;
+
+ /**
+ * The constructor.
+ * @param ctx the context
+ */
+ public VolumeTaggingMonkey(Context ctx) {
+ super(ctx);
+ this.config = ctx.configuration();
+ this.awsClient = ctx.awsClient();
+ this.calendar = ctx.calendar();
+ instanceToOwner = new HashMap();
+ for (Instance instance : awsClient.describeInstances()) {
+ for (Tag tag : instance.getTags()) {
+ if (tag.getKey().equals(JanitorMonkey.OWNER_TAG_KEY)) {
+ instanceToOwner.put(instance.getInstanceId(), tag.getValue());
+ }
+ }
+ }
+ }
+
+ /**
+ * The monkey Type.
+ */
+ public enum Type {
+ /** Volume tagging monkey. */
+ VOLUME_TAGGING
+ }
+
+ /**
+ * The event types that this monkey causes.
+ */
+ public enum EventTypes {
+ /** The event type for tagging the volume with Janitor meta data information. */
+ TAGGING_JANITOR
+ }
+
+ @Override
+ public Enum type() {
+ return Type.VOLUME_TAGGING;
+ }
+
+ @Override
+ public void doMonkeyBusiness() {
+ String prop = "simianarmy.volumeTagging.enabled";
+ if (config.getBoolOrElse(prop, false)) {
+ tagVolumesWithLatestAttachment(awsClient.describeVolumes());
+ } else {
+ LOGGER.info(String.format("Volume tagging monkey is not enabled. You can set %s to true to enalbe it.",
+ prop));
+ }
+ }
+
+ private void tagVolumesWithLatestAttachment(List volumes) {
+ LOGGER.info(String.format("Trying to tag %d volumes for Janitor Monkey meta data.",
+ volumes.size()));
+ Date now = calendar.now().getTime();
+ for (Volume volume : volumes) {
+ String owner = null, instanceId = null;
+ Date lastDetachTime = null;
+ List attachments = volume.getAttachments();
+ List tags = volume.getTags();
+
+ // The volume can have a special tag is it does not want to be changed/tagged
+ // by Jantior monkey.
+ if ("donotmark".equals(getTagValue(JanitorMonkey.JANITOR_TAG, tags))) {
+ LOGGER.info(String.format("The volume %s is tagged as not handled by Janitor",
+ volume.getVolumeId()));
+ continue;
+ }
+
+ Map janitorMetadata = parseJanitorTag(tags);
+ // finding the instance attached most recently.
+ VolumeAttachment latest = null;
+ for (VolumeAttachment attachment : attachments) {
+ if (latest == null || latest.getAttachTime().before(attachment.getAttachTime())) {
+ latest = attachment;
+ }
+ }
+ if (latest != null) {
+ instanceId = latest.getInstanceId();
+ owner = getOwnerEmail(instanceId, janitorMetadata, tags);
+ }
+
+ if (latest == null || "detached".equals(latest.getState())) {
+ if (janitorMetadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY) == null) {
+ // There is no attached instance and the last detached time is not set.
+ // Use the current time as the last detached time.
+ LOGGER.info(String.format("Setting the last detached time to %s for volume %s",
+ now, volume.getVolumeId()));
+ lastDetachTime = now;
+ } else {
+ LOGGER.debug(String.format("The volume %s was already marked as detached at time %s",
+ volume.getVolumeId(), janitorMetadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY)));
+ }
+ } else {
+ // The volume is currently attached to an instance
+ lastDetachTime = null;
+ }
+ String existingOwner = janitorMetadata.get(JanitorMonkey.OWNER_TAG_KEY);
+ if (owner == null && existingOwner != null) {
+ // Save the current owner in the tag when we are not able to find a owner.
+ owner = existingOwner;
+ }
+ if (needsUpdate(janitorMetadata, owner, instanceId, lastDetachTime)) {
+ Event evt = updateJanitorMetaTag(volume, instanceId, owner, lastDetachTime);
+ if (evt != null) {
+ context().recorder().recordEvent(evt);
+ }
+ }
+ }
+ }
+
+ private String getOwnerEmail(String instanceId, Map janitorMetadata, List tags) {
+ // The owner of the volume is set as the owner of the last instance attached to it.
+ String owner = instanceToOwner.get(instanceId);
+ if (owner == null) {
+ owner = janitorMetadata.get(JanitorMonkey.OWNER_TAG_KEY);
+ }
+ if (owner == null) {
+ owner = getTagValue(JanitorMonkey.OWNER_TAG_KEY, tags);
+ }
+ String emailDomain = getOwnerEmailDomain();
+ if (owner != null && !owner.contains("@")
+ && StringUtils.isNotBlank(emailDomain)) {
+ owner = String.format("%s@%s", owner, emailDomain);
+ }
+ return owner;
+ }
+
+ /**
+ * Parses the Janitor meta tag set by this monkey and gets a map from key
+ * to value for the tag values.
+ * @param tags the tags of the volumes
+ * @return the map from the Janitor meta tag key to value
+ */
+ private static Map parseJanitorTag(List tags) {
+ String janitorTag = getTagValue(JanitorMonkey.JANITOR_META_TAG, tags);
+ return parseJanitorMetaTag(janitorTag);
+ }
+
+ /**
+ * Parses the string of Janitor meta-data tag value to get a key value map.
+ * @param janitorMetaTag the value of the Janitor meta-data tag
+ * @return the key value map in the Janitor meta-data tag
+ */
+ public static Map parseJanitorMetaTag(String janitorMetaTag) {
+ Map metadata = new HashMap();
+ if (janitorMetaTag != null) {
+ for (String keyValue : janitorMetaTag.split(";")) {
+ String[] meta = keyValue.split("=");
+ if (meta.length == 2) {
+ metadata.put(meta[0], meta[1]);
+ }
+ }
+ }
+ return metadata;
+ }
+
+ /** Gets the domain name for the owner email. The method can be overriden in subclasses.
+ *
+ * @return the domain name for the owner email.
+ */
+ protected String getOwnerEmailDomain() {
+ return config.getStrOrElse("simianarmy.volumeTagging.ownerEmailDomain", "");
+ }
+
+ private Event updateJanitorMetaTag(Volume volume, String instance, String owner, Date lastDetachTime) {
+ String meta = makeMetaTag(instance, owner, lastDetachTime);
+ Map janitorTags = new HashMap();
+ janitorTags.put(JanitorMonkey.JANITOR_META_TAG, meta);
+ LOGGER.info(String.format("Setting tag %s to '%s' for volume %s",
+ JanitorMonkey.JANITOR_META_TAG, meta, volume.getVolumeId()));
+ String prop = "simianarmy.volumeTagging.leashed";
+ Event evt = null;
+ if (config.getBoolOrElse(prop, true)) {
+ LOGGER.info("Volume tagging monkey is leashed. No real change is made to the volume.");
+ } else {
+ try {
+ awsClient.createTagsForResources(janitorTags, volume.getVolumeId());
+ evt = context().recorder().newEvent(type(), EventTypes.TAGGING_JANITOR,
+ awsClient.region(), volume.getVolumeId());
+ evt.addField(JanitorMonkey.JANITOR_META_TAG, meta);
+ } catch (Exception e) {
+ LOGGER.error(String.format("Failed to update the tag for volume %s", volume.getVolumeId()));
+ }
+ }
+ return evt;
+ }
+
+ /**
+ * Makes the Janitor meta tag for volumes to track the last attachment/detachment information.
+ * The method is intentionally made public for testing.
+ * @param instance the last attached instance
+ * @param owner the last owner
+ * @param lastDetachTime the detach time
+ * @return the meta tag of Janitor Monkey
+ */
+ public static String makeMetaTag(String instance, String owner, Date lastDetachTime) {
+ StringBuilder meta = new StringBuilder();
+ meta.append(String.format("%s=%s;",
+ JanitorMonkey.INSTANCE_TAG_KEY, instance == null ? "" : instance));
+ meta.append(String.format("%s=%s;", JanitorMonkey.OWNER_TAG_KEY, owner == null ? "" : owner));
+ meta.append(String.format("%s=%s", JanitorMonkey.DETACH_TIME_TAG_KEY,
+ lastDetachTime == null ? "" : AWSResource.DATE_FORMATTER.print(lastDetachTime.getTime())));
+ return meta.toString();
+ }
+
+ private static String getTagValue(String key, List tags) {
+ for (Tag tag : tags) {
+ if (tag.getKey().equals(key)) {
+ return tag.getValue();
+ }
+ }
+ return null;
+ }
+
+ /** Needs to update tags of the volume if
+ * 1) owner or instance attached changed or
+ * 2) the last detached status is changed.
+ */
+ private static boolean needsUpdate(Map metadata,
+ String owner, String instance, Date lastDetachTime) {
+ return (owner != null && !StringUtils.equals(metadata.get(JanitorMonkey.OWNER_TAG_KEY), owner))
+ || (instance != null && !StringUtils.equals(metadata.get(JanitorMonkey.INSTANCE_TAG_KEY), instance))
+ || lastDetachTime != null;
+ }
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/ASGJanitorCrawler.java b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/ASGJanitorCrawler.java
new file mode 100644
index 00000000..7c41bb86
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/ASGJanitorCrawler.java
@@ -0,0 +1,181 @@
+/*
+ *
+ * Copyright 2012 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.janitor.crawler;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang.StringUtils;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.amazonaws.services.autoscaling.model.AutoScalingGroup;
+import com.amazonaws.services.autoscaling.model.Instance;
+import com.amazonaws.services.autoscaling.model.LaunchConfiguration;
+import com.amazonaws.services.autoscaling.model.SuspendedProcess;
+import com.amazonaws.services.autoscaling.model.TagDescription;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.aws.AWSResourceType;
+import com.netflix.simianarmy.client.aws.AWSClient;
+
+/**
+ * The crawler to crawl AWS auto scaling groups for janitor monkey.
+ */
+public class ASGJanitorCrawler extends AbstractAWSJanitorCrawler {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(ASGJanitorCrawler.class);
+
+ /** The name representing the additional field name of instance ids. */
+ public static final String ASG_FIELD_INSTANCES = "INSTANCES";
+
+ /** The name representing the additional field name of max ASG size. */
+ public static final String ASG_FIELD_MAX_SIZE = "MAX_SIZE";
+
+ /** The name representing the additional field name of ELB names. */
+ public static final String ASG_FIELD_ELBS = "ELBS";
+
+ /** The name representing the additional field name of launch configuration name. */
+ public static final String ASG_FIELD_LC_NAME = "LAUNCH_CONFIGURATION_NAME";
+
+ /** The name representing the additional field name of launch configuration creation time. */
+ public static final String ASG_FIELD_LC_CREATION_TIME = "LAUNCH_CONFIGURATION_CREATION_TIME";
+
+ /** The name representing the additional field name of ASG suspension time from ELB. */
+ public static final String ASG_FIELD_SUSPENSION_TIME = "ASG_SUSPENSION_TIME";
+
+ private final Map nameToLaunchConfig = new HashMap();
+
+ /** The regular expression patter below is for the termination reason added by AWS when
+ * an ASG is suspended from ELB's traffic.
+ */
+ private static final Pattern SUSPENSION_REASON_PATTERN =
+ Pattern.compile("User suspended at (\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}).*");
+
+ /** The date format used to print or parse the suspension time value. **/
+ public static final DateTimeFormatter SUSPENSION_TIME_FORMATTER =
+ DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss");
+
+ /**
+ * Instantiates a new basic ASG crawler.
+ * @param awsClient
+ * the aws client
+ */
+ public ASGJanitorCrawler(AWSClient awsClient) {
+ super(awsClient);
+ }
+
+ @Override
+ public EnumSet> resourceTypes() {
+ return EnumSet.of(AWSResourceType.ASG);
+ }
+
+ @Override
+ public List resources(Enum resourceType) {
+ if ("ASG".equals(resourceType.name())) {
+ return getASGResources();
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List resources(String... asgNames) {
+ return getASGResources(asgNames);
+ }
+
+ private List getASGResources(String... asgNames) {
+ AWSClient awsClient = getAWSClient();
+
+ List launchConfigurations = awsClient.describeLaunchConfigurations();
+ for (LaunchConfiguration lc : launchConfigurations) {
+ nameToLaunchConfig.put(lc.getLaunchConfigurationName(), lc);
+ }
+
+ List resources = new LinkedList();
+ for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups(asgNames)) {
+ Resource asgResource = new AWSResource().withId(asg.getAutoScalingGroupName())
+ .withResourceType(AWSResourceType.ASG).withRegion(awsClient.region())
+ .withLaunchTime(asg.getCreatedTime());
+ for (TagDescription tag : asg.getTags()) {
+ asgResource.setTag(tag.getKey(), tag.getValue());
+ }
+ asgResource.setDescription(String.format("%d instances", asg.getInstances().size()));
+ asgResource.setOwnerEmail(getOwnerEmailForResource(asgResource));
+ if (asg.getStatus() != null) {
+ ((AWSResource) asgResource).setAWSResourceState(asg.getStatus());
+ }
+ Integer maxSize = asg.getMaxSize();
+ if (maxSize != null) {
+ asgResource.setAdditionalField(ASG_FIELD_MAX_SIZE, String.valueOf(maxSize));
+ }
+ // Adds instances and ELBs as additional fields.
+ List instances = new ArrayList();
+ for (Instance instance : asg.getInstances()) {
+ instances.add(instance.getInstanceId());
+ }
+ asgResource.setAdditionalField(ASG_FIELD_INSTANCES, StringUtils.join(instances, ","));
+ asgResource.setAdditionalField(ASG_FIELD_ELBS,
+ StringUtils.join(asg.getLoadBalancerNames(), ","));
+ String lcName = asg.getLaunchConfigurationName();
+ LaunchConfiguration lc = nameToLaunchConfig.get(lcName);
+ if (lc != null) {
+ asgResource.setAdditionalField(ASG_FIELD_LC_NAME, lcName);
+ }
+ if (lc != null && lc.getCreatedTime() != null) {
+ asgResource.setAdditionalField(ASG_FIELD_LC_CREATION_TIME,
+ String.valueOf(lc.getCreatedTime().getTime()));
+ }
+ // sets the field for the time when the ASG's traffic is suspended from ELB
+ for (SuspendedProcess sp : asg.getSuspendedProcesses()) {
+ if ("AddToLoadBalancer".equals(sp.getProcessName())) {
+ String suspensionTime = getSuspensionTimeString(sp.getSuspensionReason());
+ if (suspensionTime != null) {
+ LOGGER.info(String.format("Suspension time of ASG %s is %s",
+ asg.getAutoScalingGroupName(), suspensionTime));
+ asgResource.setAdditionalField(ASG_FIELD_SUSPENSION_TIME, suspensionTime);
+ break;
+ }
+ }
+ }
+ resources.add(asgResource);
+ }
+ return resources;
+ }
+
+ private String getSuspensionTimeString(String suspensionReason) {
+ if (suspensionReason == null) {
+ return null;
+ }
+ Matcher matcher = SUSPENSION_REASON_PATTERN.matcher(suspensionReason);
+ if (matcher.matches()) {
+ return matcher.group(1);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/AbstractAWSJanitorCrawler.java b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/AbstractAWSJanitorCrawler.java
new file mode 100644
index 00000000..8466386a
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/AbstractAWSJanitorCrawler.java
@@ -0,0 +1,61 @@
+/*
+ *
+ * Copyright 2012 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.janitor.crawler;
+
+import org.apache.commons.lang.Validate;
+
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.client.aws.AWSClient;
+import com.netflix.simianarmy.janitor.JanitorCrawler;
+
+/**
+ * The abstract class for crawler of AWS resources.
+ */
+public abstract class AbstractAWSJanitorCrawler implements JanitorCrawler {
+ /** The AWS client. */
+ private final AWSClient awsClient;
+
+ /**
+ * The constructor.
+ * @param awsClient the AWS client used by the crawler.
+ */
+ public AbstractAWSJanitorCrawler(AWSClient awsClient) {
+ Validate.notNull(awsClient);
+ this.awsClient = awsClient;
+ }
+
+ /**
+ * Gets the owner email from the resource's tag "owner".
+ * @param resource the resource
+ * @return the owner email specified in the resource's tags
+ */
+ @Override
+ public String getOwnerEmailForResource(Resource resource) {
+ Validate.notNull(resource);
+ return resource.getTag("owner");
+ }
+
+ /**
+ * Gets the AWS client used by the crawler.
+ * @return the AWS client used by the crawler.
+ */
+ protected AWSClient getAWSClient() {
+ return awsClient;
+ }
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSSnapshotJanitorCrawler.java b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSSnapshotJanitorCrawler.java
new file mode 100644
index 00000000..8c3b86f5
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSSnapshotJanitorCrawler.java
@@ -0,0 +1,151 @@
+/*
+ *
+ * Copyright 2012 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.janitor.crawler;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.amazonaws.services.ec2.model.BlockDeviceMapping;
+import com.amazonaws.services.ec2.model.EbsBlockDevice;
+import com.amazonaws.services.ec2.model.Image;
+import com.amazonaws.services.ec2.model.Snapshot;
+import com.amazonaws.services.ec2.model.Tag;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.aws.AWSResourceType;
+import com.netflix.simianarmy.client.aws.AWSClient;
+
+/**
+ * The crawler to crawl AWS EBS snapshots for janitor monkey.
+ */
+public class EBSSnapshotJanitorCrawler extends AbstractAWSJanitorCrawler {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(EBSSnapshotJanitorCrawler.class);
+
+ /** The name representing the additional field name of AMIs genreated using the snapshot. */
+ public static final String SNAPSHOT_FIELD_AMIS = "AMIs";
+
+
+ /** The map from snapshot id to the AMI ids that are generated using the snapshot. */
+ private final Map> snapshotToAMIs =
+ new HashMap>();
+
+ /**
+ * The constructor.
+ * @param awsClient the AWS client
+ */
+ public EBSSnapshotJanitorCrawler(AWSClient awsClient) {
+ super(awsClient);
+ }
+
+ @Override
+ public EnumSet> resourceTypes() {
+ return EnumSet.of(AWSResourceType.EBS_SNAPSHOT);
+ }
+
+ @Override
+ public List resources(Enum resourceType) {
+ if ("EBS_SNAPSHOT".equals(resourceType.name())) {
+ return getSnapshotResources();
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List resources(String... resourceIds) {
+ return getSnapshotResources(resourceIds);
+ }
+
+ private List getSnapshotResources(String... snapshotIds) {
+ refreshSnapshotToAMIs();
+
+ List resources = new LinkedList();
+ AWSClient awsClient = getAWSClient();
+
+ for (Snapshot snapshot : awsClient.describeSnapshots(snapshotIds)) {
+ Resource snapshotResource = new AWSResource().withId(snapshot.getSnapshotId())
+ .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.EBS_SNAPSHOT)
+ .withLaunchTime(snapshot.getStartTime()).withDescription(snapshot.getDescription());
+ for (Tag tag : snapshot.getTags()) {
+ LOGGER.debug(String.format("Adding tag %s = %s to resource %s",
+ tag.getKey(), tag.getValue(), snapshotResource.getId()));
+ snapshotResource.setTag(tag.getKey(), tag.getValue());
+ }
+ snapshotResource.setOwnerEmail(getOwnerEmailForResource(snapshotResource));
+ ((AWSResource) snapshotResource).setAWSResourceState(snapshot.getState());
+ Collection amis = snapshotToAMIs.get(snapshotResource.getId());
+ if (amis != null) {
+ snapshotResource.setAdditionalField(SNAPSHOT_FIELD_AMIS, StringUtils.join(amis, ","));
+ }
+ resources.add(snapshotResource);
+ }
+ return resources;
+ }
+
+ @Override
+ public String getOwnerEmailForResource(Resource resource) {
+ String owner = resource.getTag("creator");
+ if (owner == null) {
+ owner = super.getOwnerEmailForResource(resource);
+ }
+ return owner;
+ }
+
+ /**
+ * Gets the collection of AMIs that are created using a specific snapshot.
+ * @param snapshotId the snapshot id
+ */
+ protected Collection getAMIsForSnapshot(String snapshotId) {
+ Collection amis = snapshotToAMIs.get(snapshotId);
+ if (amis != null) {
+ return Collections.unmodifiableCollection(amis);
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ private void refreshSnapshotToAMIs() {
+ snapshotToAMIs.clear();
+ for (Image image : getAWSClient().describeImages()) {
+ for (BlockDeviceMapping bdm : image.getBlockDeviceMappings()) {
+ EbsBlockDevice ebd = bdm.getEbs();
+ if (ebd != null && ebd.getSnapshotId() != null) {
+ LOGGER.debug(String.format("Snapshot %s is used to generate AMI %s",
+ ebd.getSnapshotId(), image.getImageId()));
+ Collection amis = snapshotToAMIs.get(ebd.getSnapshotId());
+ if (amis == null) {
+ amis = new ArrayList();
+ snapshotToAMIs.put(ebd.getSnapshotId(), amis);
+ }
+ amis.add(image.getImageId());
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSVolumeJanitorCrawler.java b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSVolumeJanitorCrawler.java
new file mode 100644
index 00000000..d41face6
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSVolumeJanitorCrawler.java
@@ -0,0 +1,117 @@
+/*
+ *
+ * Copyright 2012 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.janitor.crawler;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.amazonaws.services.ec2.model.Tag;
+import com.amazonaws.services.ec2.model.Volume;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.aws.AWSResourceType;
+import com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey;
+import com.netflix.simianarmy.client.aws.AWSClient;
+import com.netflix.simianarmy.janitor.JanitorMonkey;
+
+/**
+ * The crawler to crawl AWS EBS volumes for janitor monkey.
+ */
+public class EBSVolumeJanitorCrawler extends AbstractAWSJanitorCrawler {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(EBSVolumeJanitorCrawler.class);
+
+ /**
+ * The constructor.
+ * @param awsClient the AWS client
+ */
+ public EBSVolumeJanitorCrawler(AWSClient awsClient) {
+ super(awsClient);
+ }
+
+ @Override
+ public EnumSet> resourceTypes() {
+ return EnumSet.of(AWSResourceType.EBS_VOLUME);
+ }
+
+ @Override
+ public List resources(Enum resourceType) {
+ if ("EBS_VOLUME".equals(resourceType.name())) {
+ return getVolumeResources();
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List resources(String... resourceIds) {
+ return getVolumeResources(resourceIds);
+ }
+
+ private List getVolumeResources(String... volumeIds) {
+ List resources = new LinkedList();
+
+ AWSClient awsClient = getAWSClient();
+
+ for (Volume volume : awsClient.describeVolumes(volumeIds)) {
+ Resource volumeResource = new AWSResource().withId(volume.getVolumeId())
+ .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.EBS_VOLUME)
+ .withLaunchTime(volume.getCreateTime());
+ for (Tag tag : volume.getTags()) {
+ LOGGER.info(String.format("Adding tag %s = %s to resource %s",
+ tag.getKey(), tag.getValue(), volumeResource.getId()));
+ volumeResource.setTag(tag.getKey(), tag.getValue());
+ }
+ volumeResource.setOwnerEmail(getOwnerEmailForResource(volumeResource));
+ volumeResource.setDescription(getVolumeDescription(volume));
+ ((AWSResource) volumeResource).setAWSResourceState(volume.getState());
+ resources.add(volumeResource);
+ }
+ return resources;
+ }
+
+ private String getVolumeDescription(Volume volume) {
+ StringBuilder description = new StringBuilder();
+ Integer size = volume.getSize();
+ description.append(String.format("size=%s", size == null ? "unknown" : size));
+ for (Tag tag : volume.getTags()) {
+ description.append(String.format("; %s=%s", tag.getKey(), tag.getValue()));
+ }
+ return description.toString();
+ }
+
+ @Override
+ public String getOwnerEmailForResource(Resource resource) {
+ String owner = super.getOwnerEmailForResource(resource);
+ if (owner == null) {
+ // try to find the owner from Janitor Metadata tag set by the volume tagging monkey.
+ Map janitorTag = VolumeTaggingMonkey.parseJanitorMetaTag(resource.getTag(
+ JanitorMonkey.JANITOR_META_TAG));
+ owner = janitorTag.get(JanitorMonkey.OWNER_TAG_KEY);
+ }
+ return owner;
+ }
+
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/InstanceJanitorCrawler.java b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/InstanceJanitorCrawler.java
new file mode 100644
index 00000000..706e39e0
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/crawler/InstanceJanitorCrawler.java
@@ -0,0 +1,124 @@
+/*
+ *
+ * Copyright 2012 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.janitor.crawler;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.amazonaws.services.autoscaling.model.AutoScalingInstanceDetails;
+import com.amazonaws.services.ec2.model.Instance;
+import com.amazonaws.services.ec2.model.Tag;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.aws.AWSResourceType;
+import com.netflix.simianarmy.client.aws.AWSClient;
+
+/**
+ * The crawler to crawl AWS instances for janitor monkey.
+ */
+public class InstanceJanitorCrawler extends AbstractAWSJanitorCrawler {
+
+ /** The name representing the additional field name of ASG's name. */
+ public static final String INSTANCE_FIELD_ASG_NAME = "ASG_NAME";
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(InstanceJanitorCrawler.class);
+
+ /**
+ * Instantiates a new basic instance crawler.
+ * @param awsClient
+ * the aws client
+ */
+ public InstanceJanitorCrawler(AWSClient awsClient) {
+ super(awsClient);
+ }
+
+ @Override
+ public EnumSet> resourceTypes() {
+ return EnumSet.of(AWSResourceType.INSTANCE);
+ }
+
+ @Override
+ public List resources(Enum resourceType) {
+ if ("INSTANCE".equals(resourceType.name())) {
+ return getInstanceResources();
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List resources(String... resourceIds) {
+ return getInstanceResources(resourceIds);
+ }
+
+ private List getInstanceResources(String... instanceIds) {
+ List resources = new LinkedList();
+
+ AWSClient awsClient = getAWSClient();
+ Map idToASGInstance = new HashMap();
+ for (AutoScalingInstanceDetails instanceDetails : awsClient.describeAutoScalingInstances(instanceIds)) {
+ idToASGInstance.put(instanceDetails.getInstanceId(), instanceDetails);
+ }
+
+ for (Instance instance : awsClient.describeInstances(instanceIds)) {
+ Resource instanceResource = new AWSResource().withId(instance.getInstanceId())
+ .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.INSTANCE)
+ .withLaunchTime(instance.getLaunchTime());
+ for (Tag tag : instance.getTags()) {
+ instanceResource.setTag(tag.getKey(), tag.getValue());
+ }
+ String description = String.format("type=%s; host=%s", instance.getInstanceType(),
+ instance.getPublicDnsName() == null ? "" : instance.getPublicDnsName());
+ instanceResource.setDescription(description);
+ instanceResource.setOwnerEmail(getOwnerEmailForResource(instanceResource));
+
+ String asgName = getAsgName(instanceResource, idToASGInstance);
+ if (asgName != null) {
+ instanceResource.setAdditionalField(INSTANCE_FIELD_ASG_NAME, asgName);
+ LOGGER.info(String.format("instance %s has a ASG tag name %s.", instanceResource.getId(), asgName));
+ }
+ if (instance.getState() != null) {
+ ((AWSResource) instanceResource).setAWSResourceState(instance.getState().getName());
+ }
+ resources.add(instanceResource);
+ }
+ return resources;
+ }
+
+ private String getAsgName(Resource instanceResource, Map idToASGInstance) {
+ String asgName = instanceResource.getTag("aws:autoscaling:groupName");
+ if (asgName == null) {
+ // At most times the aws:autoscaling:groupName tag has the ASG name, but there are cases
+ // that the instance is not correctly tagged and we can find the ASG name from AutoScaling
+ // service.
+ AutoScalingInstanceDetails instanceDetails = idToASGInstance.get(instanceResource.getId());
+ if (instanceDetails != null) {
+ asgName = instanceDetails.getAutoScalingGroupName();
+ }
+ }
+ return asgName;
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/ASGInstanceValidator.java b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/ASGInstanceValidator.java
new file mode 100644
index 00000000..2a948d41
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/ASGInstanceValidator.java
@@ -0,0 +1,110 @@
+/*
+ *
+ * Copyright 2012 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.janitor.rule.asg;
+
+import java.util.List;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.appinfo.InstanceInfo;
+import com.netflix.appinfo.InstanceInfo.InstanceStatus;
+import com.netflix.discovery.DiscoveryClient;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;
+
+/**
+ * The class is for checking whether an ASG has any active instance. If Discovery/Eureka is enabled,
+ * it uses its service to check if the instances in the ASG are registered and up there.
+ */
+public class ASGInstanceValidator {
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(ASGInstanceValidator.class);
+ private final DiscoveryClient discoveryClient;
+
+ /**
+ * Constructor.
+ * @param discoveryClient
+ * the client to access the Discovery/Eureka service for checking the status of instances.
+ * If Discovery/Eureka is not enabled, the client is null.
+ */
+ public ASGInstanceValidator(DiscoveryClient discoveryClient) {
+ this.discoveryClient = discoveryClient;
+ }
+
+ /**
+ * Checks whether an ASG resource contains any active instances.
+ * @param resource the ASG resource
+ * @return true if the ASG contains any active instances, false otherwise.
+ */
+ public boolean hasActiveInstance(Resource resource) {
+ String instanceIds = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES);
+ String maxSizeStr = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE);
+ if (StringUtils.isBlank(instanceIds)) {
+ if (maxSizeStr != null && Integer.parseInt(maxSizeStr) == 0) {
+ // The ASG is empty when it has not instance and the max size of the ASG is 0.
+ // If the max size is not 0, the ASG could probably be in the process of starting new instances.
+ LOGGER.info(String.format("ASG %s is empty.", resource.getId()));
+ return false;
+ } else {
+ LOGGER.info(String.format("ASG %s does not have instances but the max size is %s",
+ resource.getId(), maxSizeStr));
+ return true;
+ }
+ }
+ String[] instances = StringUtils.split(instanceIds, ",");
+ LOGGER.debug(String.format("Checking if the %d instances in ASG %s are active.",
+ instances.length, resource.getId()));
+ for (String instanceId : instances) {
+ if (isActiveInstance(instanceId)) {
+ LOGGER.info(String.format("ASG %s has active instance.", resource.getId()));
+ return true;
+ }
+ }
+ LOGGER.info(String.format("ASG %s has no active instance.", resource.getId()));
+ return false;
+ }
+
+ /**
+ * Returns true if the instance is registered in Eureka/Discovery.
+ * @param instanceId the instance id
+ * @return true if the instance is active, false otherwise
+ */
+ private boolean isActiveInstance(String instanceId) {
+ Validate.notNull(instanceId);
+ LOGGER.debug(String.format("Checking if instance %s is active", instanceId));
+ if (discoveryClient == null) {
+ // When Discovery is not enabled, any existing instance is considered active.
+ LOGGER.debug(String.format("Instance %s is active since Discovery is not enabled.", instanceId));
+ return true;
+ } else {
+ List instanceInfos = discoveryClient.getInstancesById(instanceId);
+ for (InstanceInfo info : instanceInfos) {
+ InstanceStatus status = info.getStatus();
+ if (status == InstanceStatus.UP || status == InstanceStatus.STARTING) {
+ LOGGER.debug(String.format("Instance %s is active in Discovery.", instanceId));
+ return true;
+ }
+ }
+ }
+ LOGGER.debug(String.format("Instance %s is not active in Discovery.", instanceId));
+ return false;
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/OldEmptyASGRule.java b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/OldEmptyASGRule.java
new file mode 100644
index 00000000..a0708321
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/OldEmptyASGRule.java
@@ -0,0 +1,129 @@
+/*
+ *
+ * Copyright 2012 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.janitor.rule.asg;
+
+import java.util.Date;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.Validate;
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.discovery.DiscoveryClient;
+import com.netflix.simianarmy.MonkeyCalendar;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;
+import com.netflix.simianarmy.janitor.Rule;
+
+/**
+ * The rule for detecting the ASGs that 1) have old launch configurations and
+ * 2) do not have any instances or all instances are inactive in Eureka.
+ * 3) are not fronted with any ELBs.
+ */
+public class OldEmptyASGRule implements Rule {
+
+ private final MonkeyCalendar calendar;
+ private final int retentionDays;
+ private final int launchConfigAgeThreshold;
+ private final ASGInstanceValidator instanceValidator;
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(OldEmptyASGRule.class);
+
+ /**
+ * Constructor.
+ *
+ * @param calendar
+ * The calendar used to calculate the termination time
+ * @param retentionDays
+ * The number of days that the marked ASG is retained before being terminated
+ * @param launchConfigAgeThreshold
+ * The number of days that the launch configuration for the ASG has been created that makes the ASG be
+ * considered obsolete
+ * @param discoveryClient
+ * The Discovery client used to check if an instance is registered
+ */
+ public OldEmptyASGRule(MonkeyCalendar calendar, int launchConfigAgeThreshold, int retentionDays,
+ DiscoveryClient discoveryClient) {
+ Validate.notNull(calendar);
+ Validate.isTrue(retentionDays >= 0);
+ Validate.isTrue(launchConfigAgeThreshold >= 0);
+ this.calendar = calendar;
+ this.retentionDays = retentionDays;
+ this.launchConfigAgeThreshold = launchConfigAgeThreshold;
+ this.instanceValidator = new ASGInstanceValidator(discoveryClient);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isValid(Resource resource) {
+ Validate.notNull(resource);
+ if (!"ASG".equals(resource.getResourceType().name())) {
+ return true;
+ }
+
+ if (StringUtils.isNotEmpty(resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_ELBS))) {
+ LOGGER.info(String.format("ASG %s has ELBs.", resource.getId()));
+ return true;
+ }
+
+ if (instanceValidator.hasActiveInstance(resource)) {
+ LOGGER.info(String.format("ASG %s has active instance.", resource.getId()));
+ return true;
+ }
+
+ String lcName = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME);
+ DateTime now = new DateTime(calendar.now().getTimeInMillis());
+ if (StringUtils.isEmpty(lcName)) {
+ LOGGER.error(String.format("Failed to find launch configuration for ASG %s", resource.getId()));
+ markResource(resource, now);
+ return false;
+ }
+
+ String lcCreationTime = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME);
+ if (StringUtils.isEmpty(lcCreationTime)) {
+ LOGGER.error(String.format("Failed to find creation time for launch configuration %s", lcName));
+ return true;
+ }
+
+ DateTime createTime = new DateTime(Long.parseLong(lcCreationTime));
+ if (now.isBefore(createTime.plusDays(launchConfigAgeThreshold))) {
+ LOGGER.info(String.format("The launch configuation %s has not been created for more than %d days",
+ lcName, launchConfigAgeThreshold));
+ return true;
+ }
+ LOGGER.info(String.format("The launch configuation %s has been created for more than %d days",
+ lcName, launchConfigAgeThreshold));
+ markResource(resource, now);
+ return false;
+ }
+
+ private void markResource(Resource resource, DateTime now) {
+ if (resource.getExpectedTerminationTime() == null) {
+ Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);
+ resource.setExpectedTerminationTime(terminationTime);
+ resource.setTerminationReason(String.format(
+ "Launch config older than %d days. Not in Discovery. No ELB.",
+ launchConfigAgeThreshold + retentionDays));
+ } else {
+ LOGGER.info(String.format("Resource %s is already marked as cleanup candidate.", resource.getId()));
+ }
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/SuspendedASGRule.java b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/SuspendedASGRule.java
new file mode 100644
index 00000000..3f13f091
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/SuspendedASGRule.java
@@ -0,0 +1,111 @@
+/*
+ *
+ * Copyright 2012 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.janitor.rule.asg;
+
+import java.util.Date;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.Validate;
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.discovery.DiscoveryClient;
+import com.netflix.simianarmy.MonkeyCalendar;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;
+import com.netflix.simianarmy.janitor.Rule;
+
+/**
+ * The rule for detecting the ASGs that 1) have old launch configurations and
+ * 2) do not have any instances or all instances are inactive in Eureka.
+ * 3) are not fronted with any ELBs.
+ */
+public class SuspendedASGRule implements Rule {
+
+ private final MonkeyCalendar calendar;
+ private final int retentionDays;
+ private final int suspensionAgeThreshold;
+ private final ASGInstanceValidator instanceValidator;
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(SuspendedASGRule.class);
+
+ /**
+ * Constructor.
+ *
+ * @param calendar
+ * The calendar used to calculate the termination time
+ * @param retentionDays
+ * The number of days that the marked ASG is retained before being terminated after
+ * being marked
+ * @param suspensionAgeThreshold
+ * The number of days that the ASG has been suspended from ELB that makes the ASG be
+ * considered a cleanup candidate
+ * @param discoveryClient
+ * The Discovery client used to check if an instance is registered
+ */
+ public SuspendedASGRule(MonkeyCalendar calendar, int suspensionAgeThreshold, int retentionDays,
+ DiscoveryClient discoveryClient) {
+ Validate.notNull(calendar);
+ Validate.isTrue(retentionDays >= 0);
+ Validate.isTrue(suspensionAgeThreshold >= 0);
+ this.calendar = calendar;
+ this.retentionDays = retentionDays;
+ this.suspensionAgeThreshold = suspensionAgeThreshold;
+ this.instanceValidator = new ASGInstanceValidator(discoveryClient);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isValid(Resource resource) {
+ Validate.notNull(resource);
+ if (!"ASG".equals(resource.getResourceType().name())) {
+ return true;
+ }
+
+ if (instanceValidator.hasActiveInstance(resource)) {
+ return true;
+ }
+
+ String suspensionTimeStr = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME);
+ if (!StringUtils.isEmpty(suspensionTimeStr)) {
+ DateTime createTime = ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.parseDateTime(suspensionTimeStr);
+ DateTime now = new DateTime(calendar.now().getTimeInMillis());
+ if (now.isBefore(createTime.plusDays(suspensionAgeThreshold))) {
+ LOGGER.info(String.format("The ASG %s has not been suspended for more than %d days",
+ resource.getId(), suspensionAgeThreshold));
+ return true;
+ }
+ LOGGER.info(String.format("The ASG %s has been suspended for more than %d days",
+ resource.getId(), suspensionAgeThreshold));
+ if (resource.getExpectedTerminationTime() == null) {
+ Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);
+ resource.setExpectedTerminationTime(terminationTime);
+ resource.setTerminationReason(String.format(
+ "User suspended age more than %d days and all instances are out of service in Discovery",
+ suspensionAgeThreshold + retentionDays));
+ }
+ return false;
+ } else {
+ LOGGER.info(String.format("ASG %s is not suspended from ELB.", resource.getId()));
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/rule/instance/OrphanedInstanceRule.java b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/instance/OrphanedInstanceRule.java
new file mode 100644
index 00000000..fe43165b
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/instance/OrphanedInstanceRule.java
@@ -0,0 +1,123 @@
+/*
+ *
+ * Copyright 2012 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.janitor.rule.instance;
+
+import java.util.Date;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.Validate;
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.simianarmy.MonkeyCalendar;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler;
+import com.netflix.simianarmy.janitor.Rule;
+
+/**
+ * The rule for checking the orphaned instances that do not belong to any ASGs and
+ * launched for certain days.
+ */
+public class OrphanedInstanceRule implements Rule {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(OrphanedInstanceRule.class);
+
+ private static final String TERMINATION_REASON = "No ASG is associated with this instance";
+
+ private final MonkeyCalendar calendar;
+
+ private final int instanceAgeThreshold;
+
+ private final int retentionDaysWithOwner;
+
+ private final int retentionDaysWithoutOwner;
+
+ /**
+ * Constructor for OrphanedInstanceRule.
+ *
+ * @param calendar
+ * The calendar used to calculate the termination time
+ * @param instanceAgeThreshold
+ * The number of days that an instance is considered as orphaned since it is launched
+ * @param retentionDaysWithOwner
+ * The number of days that the orphaned instance is retained before being terminated
+ * when the instance has an owner specified
+ * @param retentionDaysWithoutOwner
+ * The number of days that the orphaned instance is retained before being terminated
+ * when the instance has no owner specified
+ */
+ public OrphanedInstanceRule(MonkeyCalendar calendar,
+ int instanceAgeThreshold, int retentionDaysWithOwner, int retentionDaysWithoutOwner) {
+ Validate.notNull(calendar);
+ Validate.isTrue(instanceAgeThreshold >= 0);
+ Validate.isTrue(retentionDaysWithOwner >= 0);
+ Validate.isTrue(retentionDaysWithoutOwner >= 0);
+ this.calendar = calendar;
+ this.instanceAgeThreshold = instanceAgeThreshold;
+ this.retentionDaysWithOwner = retentionDaysWithOwner;
+ this.retentionDaysWithoutOwner = retentionDaysWithoutOwner;
+ }
+
+ @Override
+ public boolean isValid(Resource resource) {
+ Validate.notNull(resource);
+ if (!resource.getResourceType().name().equals("INSTANCE")) {
+ // The rule is supposed to only work on AWS instances. If a non-instance resource
+ // is passed to the rule, the rule simply ignores it and considers it as a valid
+ // resource not for cleanup.
+ return true;
+ }
+ String awsStatus = ((AWSResource) resource).getAWSResourceState();
+ if (!"running".equals(awsStatus) || "pending".equals(awsStatus)) {
+ return true;
+ }
+ AWSResource instanceResource = (AWSResource) resource;
+ String asgName = instanceResource.getAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME);
+ if (StringUtils.isEmpty(asgName)) {
+ if (resource.getLaunchTime() == null) {
+ LOGGER.error(String.format("The instance %s has no launch time.", resource.getId()));
+ return true;
+ } else {
+ DateTime launchTime = new DateTime(resource.getLaunchTime().getTime());
+ DateTime now = new DateTime(calendar.now().getTimeInMillis());
+ if (now.isBefore(launchTime.plusDays(instanceAgeThreshold))) {
+ LOGGER.info(String.format("The orphaned instance %s has not launched for more than %d days",
+ resource.getId(), instanceAgeThreshold));
+ return true;
+ }
+ LOGGER.info(String.format("The orphaned instance %s has launched for more than %d days",
+ resource.getId(), instanceAgeThreshold));
+ if (resource.getExpectedTerminationTime() == null) {
+ int retentionDays = retentionDaysWithoutOwner;
+ if (resource.getOwnerEmail() != null) {
+ retentionDays = retentionDaysWithOwner;
+ }
+ Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);
+ resource.setExpectedTerminationTime(terminationTime);
+ resource.setTerminationReason(TERMINATION_REASON);
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/rule/snapshot/NoGeneratedAMIRule.java b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/snapshot/NoGeneratedAMIRule.java
new file mode 100644
index 00000000..9c949134
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/snapshot/NoGeneratedAMIRule.java
@@ -0,0 +1,142 @@
+/*
+ *
+ * Copyright 2012 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.janitor.rule.snapshot;
+
+import java.util.Date;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.Validate;
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.simianarmy.MonkeyCalendar;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.aws.janitor.crawler.EBSSnapshotJanitorCrawler;
+import com.netflix.simianarmy.janitor.JanitorMonkey;
+import com.netflix.simianarmy.janitor.Rule;
+
+/**
+ * The rule is for checking whether an EBS snapshot has any AMIs generated from it.
+ * If there are no AMIs generated using the snapshot and the snapshot is created
+ * for certain days, it is marked as a cleanup candidate by this rule.
+ */
+public class NoGeneratedAMIRule implements Rule {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(NoGeneratedAMIRule.class);
+
+ private static final String TERMINATION_REASON = "No AMI is generated for this snapshot";
+
+ private final MonkeyCalendar calendar;
+
+ private final int ageThreshold;
+
+ private final int retentionDays;
+
+ /** The date format used to print or parse the user specified termination date. **/
+ public static final DateTimeFormatter TERMINATION_DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd");
+
+ /**
+ * Constructor.
+ *
+ * @param calendar
+ * The calendar used to calculate the termination time
+ * @param ageThreshold
+ * The number of days that a snapshot is considered as cleanup candidate since it is created
+ * @param retentionDays
+ * The number of days that the volume is retained before being terminated after being marked
+ * as cleanup candidate
+ */
+ public NoGeneratedAMIRule(MonkeyCalendar calendar, int ageThreshold, int retentionDays) {
+ Validate.notNull(calendar);
+ Validate.isTrue(ageThreshold >= 0);
+ Validate.isTrue(retentionDays >= 0);
+ this.calendar = calendar;
+ this.ageThreshold = ageThreshold;
+ this.retentionDays = retentionDays;
+ }
+
+ @Override
+ public boolean isValid(Resource resource) {
+ Validate.notNull(resource);
+ if (!resource.getResourceType().name().equals("EBS_SNAPSHOT")) {
+ return true;
+ }
+ if (!"completed".equals(((AWSResource) resource).getAWSResourceState())) {
+ return true;
+ }
+ String janitorTag = resource.getTag(JanitorMonkey.JANITOR_TAG);
+ if (janitorTag != null) {
+ if ("donotmark".equals(janitorTag)) {
+ LOGGER.info(String.format("The snapshot %s is tagged as not handled by Janitor",
+ resource.getId()));
+ return true;
+ }
+ try {
+ // Owners can tag the volume with a termination date in the "janitor" tag.
+ Date userSpecifiedDate = new Date(TERMINATION_DATE_FORMATTER.parseDateTime(janitorTag).getMillis());
+ resource.setExpectedTerminationTime(userSpecifiedDate);
+ resource.setTerminationReason(String.format("User specified termination date %s", janitorTag));
+ return false;
+ } catch (Exception e) {
+ LOGGER.error(String.format("The janitor tag is not a user specified date: %s", janitorTag));
+ }
+ }
+
+ if (hasGeneratedImage(resource)) {
+ return true;
+ }
+
+ if (resource.getLaunchTime() == null) {
+ LOGGER.error(String.format("Snapshot %s does not have a creation time.", resource.getId()));
+ return true;
+ }
+ DateTime launchTime = new DateTime(resource.getLaunchTime().getTime());
+ DateTime now = new DateTime(calendar.now().getTimeInMillis());
+ if (launchTime.plusDays(ageThreshold).isBefore(now)) {
+ if (resource.getExpectedTerminationTime() == null) {
+ Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);
+ resource.setExpectedTerminationTime(terminationTime);
+ resource.setTerminationReason(TERMINATION_REASON);
+ LOGGER.info(String.format(
+ "Snapshot %s is marked to be cleaned at %s as there is no AMI generated using it",
+ resource.getId(), resource.getExpectedTerminationTime()));
+ } else {
+ LOGGER.info(String.format("Resource %s is already marked.", resource.getId()));
+ }
+ return false;
+ }
+ return true;
+
+ }
+
+ /**
+ * Gets the AMI created using the snapshot. This method can be overriden by subclasses
+ * if they use a different way to check this.
+ * @param resource the snapshot resource
+ * @return true if there are AMIs that are created using the snapshot, false otherwise
+ */
+ protected boolean hasGeneratedImage(Resource resource) {
+ return StringUtils.isNotEmpty(resource.getAdditionalField(EBSSnapshotJanitorCrawler.SNAPSHOT_FIELD_AMIS));
+ }
+
+}
diff --git a/src/main/java/com/netflix/simianarmy/aws/janitor/rule/volume/OldDetachedVolumeRule.java b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/volume/OldDetachedVolumeRule.java
new file mode 100644
index 00000000..7c5ea23b
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/aws/janitor/rule/volume/OldDetachedVolumeRule.java
@@ -0,0 +1,142 @@
+/*
+ *
+ * Copyright 2012 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.janitor.rule.volume;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.apache.commons.lang.Validate;
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.simianarmy.MonkeyCalendar;
+import com.netflix.simianarmy.Resource;
+import com.netflix.simianarmy.aws.AWSResource;
+import com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey;
+import com.netflix.simianarmy.janitor.JanitorMonkey;
+import com.netflix.simianarmy.janitor.Rule;
+
+/**
+ * The rule is for checking whether an EBS volume is detached for more than
+ * certain days. The rule mostly relies on tags on the volume to decide if
+ * the volume should be marked.
+ */
+public class OldDetachedVolumeRule implements Rule {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(OldDetachedVolumeRule.class);
+
+ private final MonkeyCalendar calendar;
+
+ private final int detachDaysThreshold;
+
+ private final int retentionDays;
+
+ /** The date format used to print or parse the user specified termination date. **/
+ public static final DateTimeFormatter TERMINATION_DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd");
+
+
+ /**
+ * Constructor.
+ *
+ * @param calendar
+ * The calendar used to calculate the termination time
+ * @param detachDaysThreshold
+ * The number of days that a volume is considered as cleanup candidate since it is detached
+ * @param retentionDays
+ * The number of days that the volume is retained before being terminated after being marked
+ * as cleanup candidate
+ */
+ public OldDetachedVolumeRule(MonkeyCalendar calendar, int detachDaysThreshold, int retentionDays) {
+ Validate.notNull(calendar);
+ Validate.isTrue(detachDaysThreshold >= 0);
+ Validate.isTrue(retentionDays >= 0);
+ this.calendar = calendar;
+ this.detachDaysThreshold = detachDaysThreshold;
+ this.retentionDays = retentionDays;
+ }
+
+ @Override
+ public boolean isValid(Resource resource) {
+ Validate.notNull(resource);
+ if (!resource.getResourceType().name().equals("EBS_VOLUME")) {
+ return true;
+ }
+ if (!"available".equals(((AWSResource) resource).getAWSResourceState())) {
+ return true;
+ }
+ String janitorTag = resource.getTag(JanitorMonkey.JANITOR_TAG);
+ if (janitorTag != null) {
+ if ("donotmark".equals(janitorTag)) {
+ LOGGER.info(String.format("The volume %s is tagged as not handled by Janitor",
+ resource.getId()));
+ return true;
+ }
+ try {
+ // Owners can tag the volume with a termination date in the "janitor" tag.
+ Date userSpecifiedDate = new Date(
+ TERMINATION_DATE_FORMATTER.parseDateTime(janitorTag).getMillis());
+ resource.setExpectedTerminationTime(userSpecifiedDate);
+ resource.setTerminationReason(String.format("User specified termination date %s", janitorTag));
+ return false;
+ } catch (Exception e) {
+ LOGGER.error(String.format("The janitor tag is not a user specified date: %s", janitorTag));
+ }
+ }
+
+ String janitorMetaTag = resource.getTag(JanitorMonkey.JANITOR_META_TAG);
+ if (janitorMetaTag == null) {
+ LOGGER.info(String.format("Volume %s is not tagged with the Janitor meta information, ignore.",
+ resource.getId()));
+ return true;
+ }
+
+ Map metadata = VolumeTaggingMonkey.parseJanitorMetaTag(janitorMetaTag);
+ String detachTimeTag = metadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY);
+ if (detachTimeTag == null) {
+ return true;
+ }
+ DateTime detachTime = null;
+ try {
+ detachTime = AWSResource.DATE_FORMATTER.parseDateTime(detachTimeTag);
+ } catch (Exception e) {
+ LOGGER.error(String.format("Detach time in the JANITOR_META tag of %s is not in the valid format: %s",
+ resource.getId(), detachTime));
+ return true;
+ }
+ DateTime now = new DateTime(calendar.now().getTimeInMillis());
+ if (detachTime != null && detachTime.plusDays(detachDaysThreshold).isBefore(now)) {
+ if (resource.getExpectedTerminationTime() == null) {
+ Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);
+ resource.setExpectedTerminationTime(terminationTime);
+ resource.setTerminationReason(String.format("Volume not attached for %d days",
+ detachDaysThreshold + retentionDays));
+ LOGGER.info(String.format(
+ "Volume %s is marked to be cleaned at %s as it is detached for more than %d days",
+ resource.getId(), resource.getExpectedTerminationTime(), detachDaysThreshold));
+ } else {
+ LOGGER.info(String.format("Resource %s is already marked.", resource.getId()));
+ }
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/basic/BasicCalendar.java b/src/main/java/com/netflix/simianarmy/basic/BasicCalendar.java
index 9e309b38..053a5340 100644
--- a/src/main/java/com/netflix/simianarmy/basic/BasicCalendar.java
+++ b/src/main/java/com/netflix/simianarmy/basic/BasicCalendar.java
@@ -18,10 +18,13 @@
package com.netflix.simianarmy.basic;
import java.util.Calendar;
+import java.util.Date;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
+import org.apache.commons.lang.Validate;
+
import com.netflix.simianarmy.Monkey;
import com.netflix.simianarmy.MonkeyCalendar;
import com.netflix.simianarmy.MonkeyConfiguration;
@@ -145,44 +148,44 @@ protected void loadHolidays(int year) {
// not be in the office to respond to rampaging monkeys
// new years, or closest work day
- holidays.add(workDayInYear(Calendar.JANUARY, 1));
+ holidays.add(workDayInYear(year, Calendar.JANUARY, 1));
// 3rd monday == MLK Day
- holidays.add(dayOfYear(Calendar.JANUARY, Calendar.MONDAY, 3));
+ holidays.add(dayOfYear(year, Calendar.JANUARY, Calendar.MONDAY, 3));
// 3rd monday == Presidents Day
- holidays.add(dayOfYear(Calendar.FEBRUARY, Calendar.MONDAY, 3));
+ holidays.add(dayOfYear(year, Calendar.FEBRUARY, Calendar.MONDAY, 3));
// last monday == Memorial Day
- holidays.add(dayOfYear(Calendar.MAY, Calendar.MONDAY, -1));
+ holidays.add(dayOfYear(year, Calendar.MAY, Calendar.MONDAY, -1));
// 4th of July, or closest work day
- holidays.add(workDayInYear(Calendar.JULY, 4));
+ holidays.add(workDayInYear(year, Calendar.JULY, 4));
// first monday == Labor Day
- holidays.add(dayOfYear(Calendar.SEPTEMBER, Calendar.MONDAY, 1));
+ holidays.add(dayOfYear(year, Calendar.SEPTEMBER, Calendar.MONDAY, 1));
// second monday == Columbus Day
- holidays.add(dayOfYear(Calendar.OCTOBER, Calendar.MONDAY, 2));
+ holidays.add(dayOfYear(year, Calendar.OCTOBER, Calendar.MONDAY, 2));
// veterans day, Nov 11th, or closest work day
- holidays.add(workDayInYear(Calendar.NOVEMBER, 11));
+ holidays.add(workDayInYear(year, Calendar.NOVEMBER, 11));
// 4th thursday == Thanksgiving
- holidays.add(dayOfYear(Calendar.NOVEMBER, Calendar.THURSDAY, 4));
+ holidays.add(dayOfYear(year, Calendar.NOVEMBER, Calendar.THURSDAY, 4));
// 4th friday == "black friday", monkey goes shopping!
- holidays.add(dayOfYear(Calendar.NOVEMBER, Calendar.FRIDAY, 4));
+ holidays.add(dayOfYear(year, Calendar.NOVEMBER, Calendar.FRIDAY, 4));
// christmas eve
- holidays.add(dayOfYear(Calendar.DECEMBER, 24));
+ holidays.add(dayOfYear(year, Calendar.DECEMBER, 24));
// christmas day
- holidays.add(dayOfYear(Calendar.DECEMBER, 25));
+ holidays.add(dayOfYear(year, Calendar.DECEMBER, 25));
// day after christmas
- holidays.add(dayOfYear(Calendar.DECEMBER, 26));
+ holidays.add(dayOfYear(year, Calendar.DECEMBER, 26));
// new years eve
- holidays.add(dayOfYear(Calendar.DECEMBER, 31));
+ holidays.add(dayOfYear(year, Calendar.DECEMBER, 31));
// mark the holiday set with the year, so on Jan 1 it will automatically
// recalculate the holidays for next year
@@ -192,14 +195,17 @@ protected void loadHolidays(int year) {
/**
* Day of year.
*
+ * @param year
+ * the year
* @param month
* the month
* @param day
* the day
* @return the day of the year
*/
- private int dayOfYear(int month, int day) {
+ private int dayOfYear(int year, int month, int day) {
Calendar holiday = now();
+ holiday.set(Calendar.YEAR, year);
holiday.set(Calendar.MONTH, month);
holiday.set(Calendar.DAY_OF_MONTH, day);
return holiday.get(Calendar.DAY_OF_YEAR);
@@ -208,6 +214,8 @@ private int dayOfYear(int month, int day) {
/**
* Day of year.
*
+ * @param year
+ * the year
* @param month
* the month
* @param dayOfWeek
@@ -216,8 +224,9 @@ private int dayOfYear(int month, int day) {
* the week in month
* @return the day of the year
*/
- private int dayOfYear(int month, int dayOfWeek, int weekInMonth) {
+ private int dayOfYear(int year, int month, int dayOfWeek, int weekInMonth) {
Calendar holiday = now();
+ holiday.set(Calendar.YEAR, year);
holiday.set(Calendar.MONTH, month);
holiday.set(Calendar.DAY_OF_WEEK, dayOfWeek);
holiday.set(Calendar.DAY_OF_WEEK_IN_MONTH, weekInMonth);
@@ -227,14 +236,17 @@ private int dayOfYear(int month, int dayOfWeek, int weekInMonth) {
/**
* Work day in year.
*
+ * @param year
+ * the year
* @param month
* the month
* @param day
* the day
* @return the day of the year adjusted to the closest workday
*/
- private int workDayInYear(int month, int day) {
+ private int workDayInYear(int year, int month, int day) {
Calendar holiday = now();
+ holiday.set(Calendar.YEAR, year);
holiday.set(Calendar.MONTH, month);
holiday.set(Calendar.DAY_OF_MONTH, day);
int doy = holiday.get(Calendar.DAY_OF_YEAR);
@@ -251,4 +263,21 @@ private int workDayInYear(int month, int day) {
return doy;
}
+ @Override
+ public Date getBusinessDay(Date date, int n) {
+ Validate.isTrue(n >= 0);
+ Calendar calendar = now();
+ calendar.setTime(date);
+ while (isHoliday(calendar) || isWeekend(calendar) || n-- > 0) {
+ calendar.add(Calendar.DATE, 1);
+ }
+ return calendar.getTime();
+ }
+
+ private boolean isWeekend(Calendar calendar) {
+ int dow = calendar.get(Calendar.DAY_OF_WEEK);
+ return dow == Calendar.SATURDAY
+ || dow == Calendar.SUNDAY;
+ }
+
}
diff --git a/src/main/java/com/netflix/simianarmy/basic/BasicChaosMonkeyContext.java b/src/main/java/com/netflix/simianarmy/basic/BasicChaosMonkeyContext.java
new file mode 100644
index 00000000..ff55c1b7
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/basic/BasicChaosMonkeyContext.java
@@ -0,0 +1,98 @@
+/*
+ *
+ * Copyright 2012 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.basic;
+
+import com.netflix.simianarmy.basic.chaos.BasicChaosInstanceSelector;
+import com.netflix.simianarmy.chaos.ChaosCrawler;
+import com.netflix.simianarmy.chaos.ChaosEmailNotifier;
+import com.netflix.simianarmy.chaos.ChaosInstanceSelector;
+import com.netflix.simianarmy.chaos.ChaosMonkey;
+import com.netflix.simianarmy.client.aws.chaos.ASGChaosCrawler;
+
+/**
+ * The Class BasicContext. This provide the basic context needed for the Chaos Monkey to run. It will configure
+ * the Chaos Monkey based on a simianarmy.properties file and chaos.properties. The properties file can be
+ * overriden with -Dsimianarmy.properties=/path/to/my.properties
+ */
+public class BasicChaosMonkeyContext extends BasicSimianArmyContext implements ChaosMonkey.Context {
+
+ /** The crawler. */
+ private ChaosCrawler crawler;
+
+ /** The selector. */
+ private ChaosInstanceSelector selector;
+
+ /** The chaos email notifier. */
+ private ChaosEmailNotifier chaosEmailNotifier;
+
+ /**
+ * Instantiates a new basic context.
+ */
+ public BasicChaosMonkeyContext() {
+ super("simianarmy.properties", "client.properties", "chaos.properties");
+ setChaosCrawler(new ASGChaosCrawler(awsClient()));
+ setChaosInstanceSelector(new BasicChaosInstanceSelector());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ChaosCrawler chaosCrawler() {
+ return crawler;
+ }
+
+ /**
+ * Sets the chaos crawler.
+ *
+ * @param chaosCrawler
+ * the new chaos crawler
+ */
+ protected void setChaosCrawler(ChaosCrawler chaosCrawler) {
+ this.crawler = chaosCrawler;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ChaosInstanceSelector chaosInstanceSelector() {
+ return selector;
+ }
+
+ /**
+ * Sets the chaos instance selector.
+ *
+ * @param chaosInstanceSelector
+ * the new chaos instance selector
+ */
+ protected void setChaosInstanceSelector(ChaosInstanceSelector chaosInstanceSelector) {
+ this.selector = chaosInstanceSelector;
+ }
+
+ @Override
+ public ChaosEmailNotifier chaosEmailNotifier() {
+ return chaosEmailNotifier;
+ }
+
+ /**
+ * Sets the chaos email notifier.
+ *
+ * @param notifier
+ * the chaos email notifier
+ */
+ protected void setChaosEmailNotifier(ChaosEmailNotifier notifier) {
+ this.chaosEmailNotifier = notifier;
+ }
+}
diff --git a/src/main/java/com/netflix/simianarmy/basic/BasicContext.java b/src/main/java/com/netflix/simianarmy/basic/BasicContext.java
deleted file mode 100644
index 7349ca68..00000000
--- a/src/main/java/com/netflix/simianarmy/basic/BasicContext.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- *
- * Copyright 2012 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.basic;
-
-import java.io.InputStream;
-import java.util.Properties;
-import java.util.concurrent.TimeUnit;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.netflix.simianarmy.aws.SimpleDBRecorder;
-import com.netflix.simianarmy.basic.chaos.BasicChaosInstanceSelector;
-import com.netflix.simianarmy.client.aws.AWSClient;
-import com.netflix.simianarmy.client.aws.chaos.ASGChaosCrawler;
-
-/**
- * The Class BasicContext. This provide the basic context needed for the monkeys to run. It will configure the monkeys
- * based on a simianarmy.properties file. The properties file can be override with
- * -Dsimianarmy.properties=/path/to/my.properties
- */
-public class BasicContext extends BasicContextShell {
-
- /** The Constant LOGGER. */
- private static final Logger LOGGER = LoggerFactory.getLogger(BasicContext.class);
-
- /** The client configuration properties. */
- private Properties props = new Properties();
-
- /** The Constant MONKEY_THREADS. */
- private static final int MONKEY_THREADS = 1;
-
- /** The client used to interact with the target machines. */
- private AWSClient awsClient;
-
- public AWSClient getAwsClient() {
- return awsClient;
- }
-
- public void setAwsClient(AWSClient awsClient) {
- this.awsClient = awsClient;
- }
-
- /** loads all relevant configuration files to configure the client. */
- protected void addClientConfigurationProperties() {
- loadClientConfigurationIntoProperties("simianarmy.properties");
- loadClientConfigurationIntoProperties("client.properties");
- }
-
- /** loads the given config ontop of the config read by previous calls. */
- protected void loadClientConfigurationIntoProperties(String propertyFileName) {
- String propFile = System.getProperty(propertyFileName, "/" + propertyFileName);
- try {
- InputStream is = BasicContext.class.getResourceAsStream(propFile);
- try {
- props.load(is);
- } finally {
- is.close();
- }
- } catch (Exception e) {
- String msg = "Unable to load properties file " + propFile + " set System property \"" + propertyFileName
- + "\" to valid file";
- LOGGER.error(msg);
- throw new RuntimeException(msg, e);
- }
- }
-
- /**
- * Instantiates a new basic context.
- */
- public BasicContext() {
- BasicConfiguration config = loadClientConfig();
-
- setCalendar(new BasicCalendar(config));
-
- createClient(config);
-
- createScheduler(config);
-
- setChaosCrawler(new ASGChaosCrawler(this.awsClient));
- setChaosInstanceSelector(new BasicChaosInstanceSelector());
-
- createRecorder(config);
- }
-
- private BasicConfiguration loadClientConfig() {
- addClientConfigurationProperties();
- BasicConfiguration config = new BasicConfiguration(props);
- setConfiguration(config);
- return config;
- }
-
- private void createScheduler(BasicConfiguration config) {
- int freq = (int) config.getNumOrElse("simianarmy.scheduler.frequency", 1);
- TimeUnit freqUnit = TimeUnit.valueOf(config.getStrOrElse("simianarmy.scheduler.frequencyUnit", "HOURS"));
- int threads = (int) config.getNumOrElse("simianarmy.scheduler.threads", MONKEY_THREADS);
- setScheduler(new BasicScheduler(freq, freqUnit, threads));
- }
-
- private void createRecorder(BasicConfiguration config) {
- String account = config.getStr("simianarmy.client.aws.accountKey");
- String secret = config.getStr("simianarmy.client.aws.secretKey");
- String region = config.getStrOrElse("simianarmy.client.aws.region", "us-east-1");
- String domain = config.getStrOrElse("simianarmy.sdb.domain", "SIMIAN_ARMY");
- setRecorder(new SimpleDBRecorder(account, secret, region, domain));
- }
-
- /**
- * Create the specific client. Override to provide your own client.
- */
- protected void createClient(BasicConfiguration config) {
- String account = config.getStr("simianarmy.client.aws.accountKey");
- String secret = config.getStr("simianarmy.client.aws.secretKey");
- String region = config.getStrOrElse("simianarmy.client.aws.region", "us-east-1");
- this.awsClient = new AWSClient(account, secret, region);
-
- setCloudClient(this.awsClient);
- }
-
-}
diff --git a/src/main/java/com/netflix/simianarmy/basic/BasicContextShell.java b/src/main/java/com/netflix/simianarmy/basic/BasicContextShell.java
deleted file mode 100644
index 884bf513..00000000
--- a/src/main/java/com/netflix/simianarmy/basic/BasicContextShell.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- *
- * Copyright 2012 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.basic;
-
-import java.util.LinkedList;
-
-import com.netflix.simianarmy.CloudClient;
-import com.netflix.simianarmy.MonkeyCalendar;
-import com.netflix.simianarmy.MonkeyConfiguration;
-import com.netflix.simianarmy.MonkeyRecorder;
-import com.netflix.simianarmy.MonkeyRecorder.Event;
-import com.netflix.simianarmy.MonkeyScheduler;
-import com.netflix.simianarmy.chaos.ChaosCrawler;
-import com.netflix.simianarmy.chaos.ChaosEmailNotifier;
-import com.netflix.simianarmy.chaos.ChaosInstanceSelector;
-import com.netflix.simianarmy.chaos.ChaosMonkey;
-
-/**
- * The Class BasicContextShell.
- */
-public class BasicContextShell implements ChaosMonkey.Context {
-
- /** The scheduler. */
- private MonkeyScheduler scheduler;
-
- /** The calendar. */
- private MonkeyCalendar calendar;
-
- /** The config. */
- private MonkeyConfiguration config;
-
- /** The client. */
- private CloudClient client;
-
- /** The crawler. */
- private ChaosCrawler crawler;
-
- /** The selector. */
- private ChaosInstanceSelector selector;
-
- /** The recorder. */
- private MonkeyRecorder recorder;
-
- /** The chaos email notifier. */
- private ChaosEmailNotifier chaosEmailNotifier;
-
- /** The reported events. */
- private LinkedList eventReport;
-
- /** protected constructor as the Shell is meant to be subclassed. */
- protected BasicContextShell() {
- eventReport = new LinkedList();
- }
-
- @Override
- public void reportEvent(Event evt) {
- this.eventReport.add(evt);
- }
-
- @Override
- public void resetEventReport() {
- eventReport.clear();
- }
-
- @Override
- public String getEventReport() {
- StringBuilder report = new StringBuilder();
-
- for (Event event : this.eventReport) {
- report.append(event.eventType()).append(" ").append(event.id()).append(" (")
- .append(event.field("groupType")).append(", ").append(event.field("groupName")).append(")\n");
- }
- return report.toString();
- }
-
- /** {@inheritDoc} */
- @Override
- public MonkeyScheduler scheduler() {
- return scheduler;
- }
-
- /**
- * Sets the scheduler.
- *
- * @param scheduler
- * the new scheduler
- */
- protected void setScheduler(MonkeyScheduler scheduler) {
- this.scheduler = scheduler;
- }
-
- /** {@inheritDoc} */
- @Override
- public MonkeyCalendar calendar() {
- return calendar;
- }
-
- /**
- * Sets the calendar.
- *
- * @param calendar
- * the new calendar
- */
- protected void setCalendar(MonkeyCalendar calendar) {
- this.calendar = calendar;
- }
-
- /** {@inheritDoc} */
- @Override
- public MonkeyConfiguration configuration() {
- return config;
- }
-
- /**
- * Sets the configuration.
- *
- * @param configuration
- * the new configuration
- */
- protected void setConfiguration(MonkeyConfiguration configuration) {
- this.config = configuration;
- }
-
- /** {@inheritDoc} */
- @Override
- public CloudClient cloudClient() {
- return client;
- }
-
- /**
- * Sets the cloud client.
- *
- * @param cloudClient
- * the new cloud client
- */
- protected void setCloudClient(CloudClient cloudClient) {
- this.client = cloudClient;
- }
-
- /** {@inheritDoc} */
- @Override
- public ChaosCrawler chaosCrawler() {
- return crawler;
- }
-
- /**
- * Sets the chaos crawler.
- *
- * @param chaosCrawler
- * the new chaos crawler
- */
- protected void setChaosCrawler(ChaosCrawler chaosCrawler) {
- this.crawler = chaosCrawler;
- }
-
- /** {@inheritDoc} */
- @Override
- public ChaosInstanceSelector chaosInstanceSelector() {
- return selector;
- }
-
- /**
- * Sets the chaos instance selector.
- *
- * @param chaosInstanceSelector
- * the new chaos instance selector
- */
- protected void setChaosInstanceSelector(ChaosInstanceSelector chaosInstanceSelector) {
- this.selector = chaosInstanceSelector;
- }
-
- /** {@inheritDoc} */
- @Override
- public MonkeyRecorder recorder() {
- return recorder;
- }
-
- /**
- * Sets the recorder.
- *
- * @param recorder
- * the new recorder
- */
- protected void setRecorder(MonkeyRecorder recorder) {
- this.recorder = recorder;
- }
-
- @Override
- public ChaosEmailNotifier chaosEmailNotifier() {
- return chaosEmailNotifier;
- }
-
- /**
- * Sets the chaos email notifier.
- *
- * @param notifier
- * the chaos email notifier
- */
- protected void setChaosEmailNotifier(ChaosEmailNotifier notifier) {
- this.chaosEmailNotifier = notifier;
- }
-
-}
diff --git a/src/main/java/com/netflix/simianarmy/basic/BasicMonkeyServer.java b/src/main/java/com/netflix/simianarmy/basic/BasicMonkeyServer.java
index 6ed6cd60..7b0aefe6 100644
--- a/src/main/java/com/netflix/simianarmy/basic/BasicMonkeyServer.java
+++ b/src/main/java/com/netflix/simianarmy/basic/BasicMonkeyServer.java
@@ -28,7 +28,11 @@
import org.slf4j.LoggerFactory;
import com.netflix.simianarmy.MonkeyRunner;
+import com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey;
import com.netflix.simianarmy.basic.chaos.BasicChaosMonkey;
+import com.netflix.simianarmy.basic.janitor.BasicJanitorMonkey;
+import com.netflix.simianarmy.basic.janitor.BasicJanitorMonkeyContext;
+import com.netflix.simianarmy.basic.janitor.BasicVolumeTaggingMonkeyContext;
/**
* Will periodically run the configured monkeys.
@@ -44,17 +48,22 @@ public class BasicMonkeyServer extends HttpServlet {
*/
@SuppressWarnings("unchecked")
public void addMonkeysToRun() {
- RUNNER.replaceMonkey(getMonkeyClass(), this.clientContextClass);
+ LOGGER.info("Adding Chaos Monkey.");
+ RUNNER.replaceMonkey(getChaosMonkeyClass(), this.chaosContextClass);
+ LOGGER.info("Adding Volume Tagging Monkey.");
+ RUNNER.replaceMonkey(VolumeTaggingMonkey.class, BasicVolumeTaggingMonkeyContext.class);
+ LOGGER.info("Adding Janitor Monkey.");
+ RUNNER.replaceMonkey(BasicJanitorMonkey.class, BasicJanitorMonkeyContext.class);
}
/**
* make the class of the client object configurable.
*/
@SuppressWarnings("rawtypes")
- private Class clientContextClass = com.netflix.simianarmy.basic.BasicContext.class;
+ private Class chaosContextClass = com.netflix.simianarmy.basic.BasicChaosMonkeyContext.class;
@SuppressWarnings("rawtypes")
- protected Class getMonkeyClass() {
+ protected Class getChaosMonkeyClass() {
return BasicChaosMonkey.class;
}
@@ -84,11 +93,11 @@ private void loadClientContextClass(Properties clientConfig) throws ServletExcep
try {
String clientContextClassName = clientConfig.getProperty(clientContextClassKey);
if (clientContextClassName == null || clientContextClassName.isEmpty()) {
- LOGGER.info("using standard client " + this.clientContextClass.getCanonicalName());
+ LOGGER.info("using standard client " + this.chaosContextClass.getCanonicalName());
return;
}
- this.clientContextClass = classLoader.loadClass(clientContextClassName);
- LOGGER.info("as " + clientContextClassKey + " loaded " + clientContextClass.getCanonicalName());
+ this.chaosContextClass = classLoader.loadClass(clientContextClassName);
+ LOGGER.info("as " + clientContextClassKey + " loaded " + chaosContextClass.getCanonicalName());
} catch (ClassNotFoundException e) {
throw new ServletException("Could not load " + clientContextClassKey, e);
}
@@ -126,7 +135,12 @@ private Properties loadClientConfigProperties() throws ServletException {
@Override
public void destroy() {
RUNNER.stop();
- RUNNER.removeMonkey(getMonkeyClass());
+ LOGGER.info("Stopping Chaos Monkey.");
+ RUNNER.removeMonkey(getChaosMonkeyClass());
+ LOGGER.info("Stopping volume tagging Monkey.");
+ RUNNER.removeMonkey(VolumeTaggingMonkey.class);
+ LOGGER.info("Stopping Janitor Monkey.");
+ RUNNER.removeMonkey(BasicJanitorMonkey.class);
super.destroy();
}
}
diff --git a/src/main/java/com/netflix/simianarmy/basic/BasicSimianArmyContext.java b/src/main/java/com/netflix/simianarmy/basic/BasicSimianArmyContext.java
new file mode 100644
index 00000000..5cb84d20
--- /dev/null
+++ b/src/main/java/com/netflix/simianarmy/basic/BasicSimianArmyContext.java
@@ -0,0 +1,296 @@
+/*
+ *
+ * Copyright 2012 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.basic;
+
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.netflix.simianarmy.CloudClient;
+import com.netflix.simianarmy.Monkey;
+import com.netflix.simianarmy.MonkeyCalendar;
+import com.netflix.simianarmy.MonkeyConfiguration;
+import com.netflix.simianarmy.MonkeyRecorder;
+import com.netflix.simianarmy.MonkeyRecorder.Event;
+import com.netflix.simianarmy.MonkeyScheduler;
+import com.netflix.simianarmy.aws.SimpleDBRecorder;
+import com.netflix.simianarmy.client.aws.AWSClient;
+
+/**
+ * The Class BasicSimianArmyContext.
+ */
+public class BasicSimianArmyContext implements Monkey.Context {
+
+ /** The Constant LOGGER. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(BasicSimianArmyContext.class);
+
+ /** The configuration properties. */
+ private final Properties properties = new Properties();
+
+ /** The Constant MONKEY_THREADS. */
+ private static final int MONKEY_THREADS = 1;
+
+ /** The scheduler. */
+ private MonkeyScheduler scheduler;
+
+ /** The calendar. */
+ private MonkeyCalendar calendar;
+
+ /** The config. */
+ private BasicConfiguration config;
+
+ /** The client. */
+ private AWSClient client;
+
+ /** The recorder. */
+ private MonkeyRecorder recorder;
+
+ /** The reported events. */
+ private final LinkedList eventReport;
+
+ private final String account;
+
+ private final String secret;
+
+ private final String region;
+
+ /** protected constructor as the Shell is meant to be subclassed. */
+ protected BasicSimianArmyContext(String... configFiles) {
+ eventReport = new LinkedList();
+ // Load the config files into props following the provided order.
+ for (String configFile : configFiles) {
+ loadConfigurationFileIntoProperties(configFile);
+ }
+ LOGGER.info("The folowing are properties in the context.");
+ for (Entry
");
+ return table.toString();
+ }
+
+ @Override
+ protected String getFooter() {
+ return " Janitor Monkey wiki: https://github.com/Netflix/SimianArmy/wiki ";
+ }
+
+ /**
+ * Gets the url to view the details of the resource.
+ * @param resource the resource
+ * @return the url to view/edit the resource.
+ */
+ protected String getResourceUrl(Resource resource) {
+ return null;
+ }
+
+ /**
+ * Gets the string when displaying the resource, e.g. the id.
+ * @param resource the resource to display
+ * @return the string to represent the resource
+ */
+ protected String getResourceDisplay(Resource resource) {
+ return resource.getId();
+ }
+
+ /**
+ * Gets the url to edit the Janitor termination of the resource.
+ * @param resource the resource
+ * @return the url to edit the Janitor termination the resource.
+ */
+ protected String getJanitorResourceUrl(Resource resource) {
+ return null;
+ }
+
+ /** Gets the table columns for the table in the email.
+ *
+ * @return the array of column names
+ */
+ protected String[] getTableColumns() {
+ return TABLE_COLUMNS;
+ }
+
+ /**
+ * Gets the row for a resource in the table in the email body.
+ * @param resource the resource to display
+ * @return the table row in the email body
+ */
+ protected String getResourceRow(Resource resource) {
+ StringBuilder message = new StringBuilder();
+ message.append("