[
  {
    "path": ".gitignore",
    "content": "# Compiled source #\n###################\n*.com\n*.class\n*.dll\n*.exe\n*.o\n*.so\n\n# Packages #\n############\n# it's better to unpack these files and commit the raw source\n# git has its own built in compression methods\n*.7z\n*.dmg\n*.gz\n*.iso\n*.jar\n*.rar\n*.tar\n*.zip\n\n# Logs and databases #\n######################\n*.log\n\n# OS generated files #\n######################\n.DS_Store*\nehthumbs.db\nIcon?\nThumbs.db\n\n# Editor Files #\n################\n*~\n*.swp\n\n# Gradle Files #\n################\n.gradle\n\n# Build output directies\n/target\n*/target\n/build\n*/build\n/bin\n*/bin\n/test-output\n*/test-output\n\n# IntelliJ specific files/directories\nout\n.idea\n*.ipr\n*.iws\n*.iml\natlassian-ide-plugin.xml\n\n# Eclipse specific files/directories\n.classpath\n.project\n.settings\n.metadata\n\n# NetBeans specific files/directories\n.nbattrs\n/bin\n/test-output\n"
  },
  {
    "path": ".netflixoss",
    "content": "cloudbees_disabled=true\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: java\njdk:\n- oraclejdk8\nsudo: required\ndist: trusty\ninstall: ./installViaTravis.sh\nscript: ./buildViaTravis.sh\ncache:\n  directories:\n  - $HOME/.gradle/caches\nenv:\n  global:\n  - secure: WCRqvIKdPdIsoDhsJWZNBZhEH7Jdgz2fmkjzozVjs4dq36ySrH71udNtZcPIsTwjmHpRaGX0XCgmwLC5WorS2TBJJ87oghCP3WWQGMBLcCdXHS8quRdAHLHpNfao/BQrBEA/gmCYoJZdmXKFDc+XKXS5NBrHkkvVfLGCumcP0AI=\n  - secure: TKnGiZyCtWWI/ei2lNDvGIjyAI4W8xMNOlXT6tGiWJgexvFQpTl2NgkMqgwbxReyxj37vdUnn9Lb/883G6zL/uB+l5aCjeCG//6GAbJYdrSZQCE/UCo7iMlAxyqfuIlKcJABIhwpP8Fg4RwqxJG19Tbx5ddg8RP8yKAi1QNx06Y=\n  - secure: nUn8s+1fV60Hxb9V9DouFIOGHeBpeTD7l6Yadw4gthvi/tZndZ+L/Crh1Z9pAU69NqEHG/VcFLUMNER7dQ4rugVbcbfQueeCdnVpmStLS97tAl8kArhpWCk8dQi47IANuQw7U0nVlg3pA8w9HLZX6ee9PnhyG1oOnluPC/x2Or4=\n  - secure: KTtxnPJWfkwNwYkd2IxKAc4dUc6jF0Fd6uhrqK5q36z0RnY4b/gKlx8bjGPcZA5hutNmiN/gxyvpbL/bvVg9buQ2vkybaPZpzpLwhHTXiD5accjQUMuwF8DFYpzIb104hkgzHbrW18JRImK539ib5TTanF3I08F04LssSXG8NnY=\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": ""
  },
  {
    "path": "GNUmakefile",
    "content": "reformat:\n\teclipse -nosplash -application org.eclipse.jdt.core.JavaCodeFormatter -verbose -config $(shell pwd)/codequality/org.eclipse.jdt.core.prefs $(shell pwd)/src\n\tfind $(shell pwd)/src -name \\*.java | xargs perl -pi -e 's/{ /{/g; s/(\\S) }/$$1}/g; s/\\* $$/\\*/; s/([.]<[^>]+>)\\s+/$$1/g'\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2012 Netflix, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "OSSMETADATA",
    "content": "osslifecycle=archived\n"
  },
  {
    "path": "README.md",
    "content": "[![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/SimianArmy.svg)](OSSMETADATA)\n[![Build Status](https://travis-ci.org/Netflix/SimianArmy.svg?branch=master)](https://travis-ci.org/Netflix/SimianArmy)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\n## PROJECT STATUS: RETIRED\n\n**The Simian Army project is no longer actively maintained**. Some of the Simian\nArmy functionality has been moved to other Netflix projects:\n\n* A [newer version of Chaos Monkey](https://github.com/netflix/chaosmonkey) is available as a standalone service.\n* [Swabbie] is a new standalone service that will replace the functionality provided by Janitor Monkey.\n* Conformity Monkey functionality will be rolled into other [Spinnaker] backend services.\n\n\n[Swabbie]: https://github.com/spinnaker/swabbie\n[Spinnaker]: https://www.spinnaker.io/\n\n### DESCRIPTION\n\nThe Simian Army is a suite of tools for keeping your cloud operating in top\nform.  Chaos Monkey, the first member, is a resiliency tool that helps ensure\nthat your applications can tolerate random instance failures\n\n\n### DETAILS\n\nPlease see the [wiki](https://github.com/Netflix/SimianArmy/wiki).\n\n### SUPPORT\n\n[Simian Army Google group](http://groups.google.com/group/simianarmy-users)\n\nBecause the project is no longer maintained, there is a good chance that nobody will be able to answer a support question.\n\n### LICENSE\n\nCopyright 2012-2016 Netflix, Inc.\n\nLicensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in\ncompliance with the License. You may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software distributed under the License is\ndistributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\nimplied. See the License for the specific language governing permissions and limitations under the\nLicense.\n"
  },
  {
    "path": "build.gradle",
    "content": "buildscript {\n    repositories {\n        jcenter()\n    }\n}\n\nplugins {\n    id 'nebula.netflixoss' version '3.2.3'\n    id 'net.saliman.cobertura' version '2.2.7'\n    id 'com.github.hierynomus.license' version '0.11.0'\n}\n\n// Establish version and status\next.githubProjectName = 'SimianArmy'\ngroup = \"com.netflix.${project.name}\"\napply plugin:'eclipse-wtp'\n\nrepositories {\n    mavenLocal()\n    mavenCentral()\n}\n\napply plugin: 'war'\napply plugin: 'jetty'\n\nsourceCompatibility = 1.8\ntargetCompatibility = 1.8\n\ndependencies {\n    // for the VMWareClient\n    compile 'com.cloudbees.thirdparty:vijava:5.0.0'\n\n    // for DB support outside of AWS (SimpleDB not available)\n    compile 'org.mapdb:mapdb:0.9.5'\n\n    compile 'com.sun.jersey:jersey-servlet:1.19'\n    compile 'org.slf4j:slf4j-api:1.7.2'\n    compile 'org.codehaus.jackson:jackson-core-asl:1.9.2'\n    compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.2'\n    compile 'com.netflix.eureka:eureka-client:1.4.1'\n    compile 'com.amazonaws:aws-java-sdk:1.11.28'\n    compile 'commons-lang:commons-lang:2.6'\n    compile 'com.google.guava:guava:11.0.2'\n    compile 'org.apache.httpcomponents:httpclient:4.3'\n    compile 'com.google.auto.service:auto-service:1.0-rc2'\n    compile 'org.apache.jclouds.driver:jclouds-jsch:1.9.0'\n    compile 'org.apache.jclouds.driver:jclouds-slf4j:1.9.0'\n    compile 'org.apache.jclouds.api:ec2:1.9.0'\n    compile 'org.apache.jclouds.provider:aws-ec2:1.9.0'\n    compile 'com.netflix.servo:servo-core:0.12.11'\n    compile 'org.springframework:spring-jdbc:4.2.5.RELEASE'\n    compile 'com.zaxxer:HikariCP:2.4.7'\n\n    testCompile 'org.testng:testng:6.3.1'\n    testCompile 'org.mockito:mockito-core:1.8.5'\n\n    runtime 'org.slf4j:slf4j-log4j12:1.6.1'\n\n    providedCompile 'javax.servlet:servlet-api:2.5'\n}\n\ntest {\n    useTestNG()\n}\n\ntasks.withType(JavaCompile) {\n    options.compilerArgs << \"-Xlint\"\n}\n\nlicense {\n    exclude '**/*.properties'\n    exclude '**/*.json'\n    exclude '**/*.sh'\n}\n\ntask coreJar(type: Jar) {\n    from sourceSets.main.output\n    include '**'\n}\n\npublishing {\n    publications {\n        mavenWar(MavenPublication) {\n            from components.web\n            artifact coreJar { classifier \"core\" }\n        }\n    }\n}\n\nartifactoryPublish {\n    publications('mavenWar')\n}\n"
  },
  {
    "path": "buildViaTravis.sh",
    "content": "#!/bin/bash\n# This script will build the project.\n\nif [ \"$TRAVIS_PULL_REQUEST\" != \"false\" ]; then\n  echo -e \"Build Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]\"\n  ./gradlew clean build\nelif [ \"$TRAVIS_PULL_REQUEST\" == \"false\" ] && [ \"$TRAVIS_TAG\" == \"\" ]; then\n  echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']'\n  ./gradlew -Prelease.travisci=true -PbintrayUser=\"${bintrayUser}\" -PbintrayKey=\"${bintrayKey}\" -PsonatypeUsername=\"${sonatypeUsername}\" -PsonatypePassword=\"${sonatypePassword}\" build snapshot \nelif [ \"$TRAVIS_PULL_REQUEST\" == \"false\" ] && [ \"$TRAVIS_TAG\" != \"\" ]; then\n  echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH']  Tag ['$TRAVIS_TAG']'\n  case \"$TRAVIS_TAG\" in\n  *-rc\\.*)\n    ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser=\"${bintrayUser}\" -PbintrayKey=\"${bintrayKey}\" -PsonatypeUsername=\"${sonatypeUsername}\" -PsonatypePassword=\"${sonatypePassword}\" candidate\n    ;;\n  *)\n    ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser=\"${bintrayUser}\" -PbintrayKey=\"${bintrayKey}\" -PsonatypeUsername=\"${sonatypeUsername}\" -PsonatypePassword=\"${sonatypePassword}\" final\n    ;;\n  esac\nelse\n  echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH']  Tag ['$TRAVIS_TAG']  Pull Request ['$TRAVIS_PULL_REQUEST']'\n  ./gradlew build\nfi\n"
  },
  {
    "path": "codequality/checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n    \"-//Puppy Crawl//DTD Check Configuration 1.2//EN\"\n    \"http://www.puppycrawl.com/dtds/configuration_1_2.dtd\">\n\n<module name=\"Checker\">\n\n    <!-- Checks that a package-info.java file exists for each package.     -->\n    <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage -->\n    <!--\n    <module name=\"JavadocPackage\">\n      <property name=\"allowLegacy\" value=\"true\"/>\n    </module>\n    -->\n\n    <!-- Checks whether files end with a new line.                        -->\n    <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->\n    <!--module name=\"NewlineAtEndOfFile\"/-->\n\n    <!-- Checks that property files contain the same keys.         -->\n    <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->\n    <module name=\"Translation\"/>\n\n    <!-- Checks for Size Violations.                    -->\n    <!-- See http://checkstyle.sf.net/config_sizes.html -->\n    <module name=\"FileLength\"/>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n    <module name=\"FileTabCharacter\"/>\n\n    <!-- Miscellaneous other checks.                   -->\n    <!-- See http://checkstyle.sf.net/config_misc.html -->\n    <module name=\"RegexpSingleline\">\n       <property name=\"format\" value=\"\\s+$\"/>\n       <property name=\"minimum\" value=\"0\"/>\n       <property name=\"maximum\" value=\"0\"/>\n       <property name=\"message\" value=\"Line has trailing spaces.\"/>\n       <property name=\"severity\" value=\"info\"/>\n    </module>\n\n    <module name=\"TreeWalker\">\n\n        <!-- Checks for Javadoc comments.                     -->\n        <!-- See http://checkstyle.sf.net/config_javadoc.html -->\n        <module name=\"JavadocMethod\">\n          <property name=\"scope\" value=\"package\"/>\n          <property name=\"allowMissingParamTags\" value=\"true\"/>\n          <property name=\"allowMissingThrowsTags\" value=\"true\"/>\n          <property name=\"allowMissingReturnTag\" value=\"true\"/>\n          <property name=\"allowThrowsTagsForSubclasses\" value=\"true\"/>\n          <property name=\"allowUndeclaredRTE\" value=\"true\"/>\n          <property name=\"allowMissingPropertyJavadoc\" value=\"true\"/>\n        </module>\n        <module name=\"JavadocType\">\n          <property name=\"scope\" value=\"package\"/>\n        </module>\n        <module name=\"JavadocVariable\">\n          <property name=\"scope\" value=\"package\"/>\n        </module>\n        <module name=\"JavadocStyle\">\n          <property name=\"checkEmptyJavadoc\" value=\"true\"/>\n        </module>\n\n        <!-- Checks for Naming Conventions.                  -->\n        <!-- See http://checkstyle.sf.net/config_naming.html -->\n        <module name=\"ConstantName\"/>\n        <module name=\"LocalFinalVariableName\"/>\n        <module name=\"LocalVariableName\"/>\n        <module name=\"MemberName\"/>\n        <module name=\"MethodName\"/>\n        <module name=\"PackageName\"/>\n        <module name=\"ParameterName\"/>\n        <module name=\"StaticVariableName\"/>\n        <module name=\"TypeName\"/>\n\n        <!-- Checks for imports                              -->\n        <!-- See http://checkstyle.sf.net/config_import.html -->\n        <module name=\"AvoidStarImport\"/>\n        <module name=\"IllegalImport\"/> <!-- defaults to sun.* packages -->\n        <module name=\"RedundantImport\"/>\n        <module name=\"UnusedImports\"/>\n\n\n        <!-- Checks for Size Violations.                    -->\n        <!-- See http://checkstyle.sf.net/config_sizes.html -->\n        <module name=\"LineLength\">\n          <!-- what is a good max value? -->\n          <property name=\"max\" value=\"120\"/>\n          <!-- ignore lines like \"$File: //depot/... $\" -->\n          <property name=\"ignorePattern\" value=\"\\$File.*\\$\"/>\n          <property name=\"severity\" value=\"info\"/>\n        </module>\n        <module name=\"MethodLength\"/>\n        <module name=\"ParameterNumber\"/>\n\n\n        <!-- Checks for whitespace                               -->\n        <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n        <module name=\"EmptyForIteratorPad\"/>\n        <module name=\"GenericWhitespace\"/>\n        <module name=\"MethodParamPad\"/>\n        <module name=\"NoWhitespaceAfter\"/>\n        <module name=\"NoWhitespaceBefore\"/>\n        <module name=\"OperatorWrap\"/>\n        <module name=\"ParenPad\"/>\n        <module name=\"TypecastParenPad\"/>\n        <module name=\"WhitespaceAfter\"/>\n        <module name=\"WhitespaceAround\"/>\n\n        <!-- Modifier Checks                                    -->\n        <!-- See http://checkstyle.sf.net/config_modifiers.html -->\n        <module name=\"ModifierOrder\"/>\n        <module name=\"RedundantModifier\"/>\n\n\n        <!-- Checks for blocks. You know, those {}'s         -->\n        <!-- See http://checkstyle.sf.net/config_blocks.html -->\n        <module name=\"AvoidNestedBlocks\"/>\n        <module name=\"EmptyBlock\">\n          <property name=\"option\" value=\"text\"/>\n        </module>\n        <module name=\"LeftCurly\"/>\n        <module name=\"NeedBraces\"/>\n        <module name=\"RightCurly\"/>\n\n\n        <!-- Checks for common coding problems               -->\n        <!-- See http://checkstyle.sf.net/config_coding.html -->\n        <!-- <module name=\"AvoidInlineConditionals\"/> -->\n        <module name=\"EmptyStatement\"/>\n        <module name=\"EqualsHashCode\"/>\n        <module name=\"HiddenField\">\n          <property name=\"ignoreConstructorParameter\" value=\"true\"/>\n          <property name=\"ignoreSetter\" value=\"true\"/>\n          <property name=\"severity\" value=\"warning\"/>\n        </module>\n        <module name=\"IllegalInstantiation\"/>\n        <module name=\"InnerAssignment\"/>\n        <module name=\"MagicNumber\">\n          <property name=\"severity\" value=\"warning\"/>\n        </module>\n        <module name=\"MissingSwitchDefault\"/>\n        <!-- Problem with finding exception types... -->\n        <module name=\"RedundantThrows\">\n          <property name=\"allowUnchecked\" value=\"true\"/>\n          <property name=\"suppressLoadErrors\" value=\"true\"/>\n          <property name=\"severity\" value=\"info\"/>\n        </module>\n        <module name=\"SimplifyBooleanExpression\"/>\n        <module name=\"SimplifyBooleanReturn\"/>\n\n        <!-- Checks for class design                         -->\n        <!-- See http://checkstyle.sf.net/config_design.html -->\n        <!-- <module name=\"DesignForExtension\"/> -->\n        <module name=\"FinalClass\"/>\n        <module name=\"HideUtilityClassConstructor\"/>\n        <module name=\"InterfaceIsType\"/>\n        <module name=\"VisibilityModifier\"/>\n\n\n        <!-- Miscellaneous other checks.                   -->\n        <!-- See http://checkstyle.sf.net/config_misc.html -->\n        <module name=\"ArrayTypeStyle\"/>\n        <!-- <module name=\"FinalParameters\"/> -->\n        <module name=\"TodoComment\">\n          <property name=\"format\" value=\"TODO\"/>\n          <property name=\"severity\" value=\"info\"/>\n        </module>\n        <module name=\"UpperEll\"/>\n\n        <module name=\"FileContentsHolder\"/> <!-- Required by comment suppression filters -->\n\n    </module>\n\n    <!-- Enable suppression comments -->\n    <module name=\"SuppressionCommentFilter\">\n      <property name=\"offCommentFormat\" value=\"CHECKSTYLE IGNORE\\s+(\\S+)\"/>\n      <property name=\"onCommentFormat\" value=\"CHECKSTYLE END IGNORE\\s+(\\S+)\"/>\n      <property name=\"checkFormat\" value=\"$1\"/>\n    </module>\n    <module name=\"SuppressWithNearbyCommentFilter\">\n      <!-- Syntax is \"SUPPRESS CHECKSTYLE name\" -->\n      <property name=\"commentFormat\" value=\"SUPPRESS CHECKSTYLE (\\w+)\"/>\n      <property name=\"checkFormat\" value=\"$1\"/>\n      <property name=\"influenceFormat\" value=\"1\"/>\n    </module>\n</module>\n"
  },
  {
    "path": "codequality/org.eclipse.jdt.core.prefs",
    "content": "eclipse.preferences.version=1\norg.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled\norg.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6\norg.eclipse.jdt.core.compiler.compliance=1.6\norg.eclipse.jdt.core.compiler.problem.assertIdentifier=error\norg.eclipse.jdt.core.compiler.problem.enumIdentifier=error\norg.eclipse.jdt.core.compiler.source=1.6\norg.eclipse.jdt.core.formatter.align_type_members_on_columns=false\norg.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16\norg.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0\norg.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16\norg.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16\norg.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16\norg.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16\norg.eclipse.jdt.core.formatter.alignment_for_assignment=0\norg.eclipse.jdt.core.formatter.alignment_for_binary_expression=16\norg.eclipse.jdt.core.formatter.alignment_for_compact_if=16\norg.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80\norg.eclipse.jdt.core.formatter.alignment_for_enum_constants=0\norg.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16\norg.eclipse.jdt.core.formatter.alignment_for_method_declaration=0\norg.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16\norg.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16\norg.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16\norg.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80\norg.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16\norg.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16\norg.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16\norg.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16\norg.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16\norg.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16\norg.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16\norg.eclipse.jdt.core.formatter.blank_lines_after_imports=1\norg.eclipse.jdt.core.formatter.blank_lines_after_package=1\norg.eclipse.jdt.core.formatter.blank_lines_before_field=0\norg.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0\norg.eclipse.jdt.core.formatter.blank_lines_before_imports=1\norg.eclipse.jdt.core.formatter.blank_lines_before_member_type=1\norg.eclipse.jdt.core.formatter.blank_lines_before_method=1\norg.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1\norg.eclipse.jdt.core.formatter.blank_lines_before_package=0\norg.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1\norg.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1\norg.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line\norg.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line\norg.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false\norg.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false\norg.eclipse.jdt.core.formatter.comment.format_block_comments=true\norg.eclipse.jdt.core.formatter.comment.format_header=false\norg.eclipse.jdt.core.formatter.comment.format_html=true\norg.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true\norg.eclipse.jdt.core.formatter.comment.format_line_comments=true\norg.eclipse.jdt.core.formatter.comment.format_source_code=true\norg.eclipse.jdt.core.formatter.comment.indent_parameter_description=true\norg.eclipse.jdt.core.formatter.comment.indent_root_tags=true\norg.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert\norg.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert\norg.eclipse.jdt.core.formatter.comment.line_length=120\norg.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true\norg.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true\norg.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false\norg.eclipse.jdt.core.formatter.compact_else_if=true\norg.eclipse.jdt.core.formatter.continuation_indentation=2\norg.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2\norg.eclipse.jdt.core.formatter.disabling_tag=@formatter\\:off\norg.eclipse.jdt.core.formatter.enabling_tag=@formatter\\:on\norg.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false\norg.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true\norg.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true\norg.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true\norg.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true\norg.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true\norg.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true\norg.eclipse.jdt.core.formatter.indent_empty_lines=false\norg.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true\norg.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true\norg.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true\norg.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false\norg.eclipse.jdt.core.formatter.indentation.size=4\norg.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert\norg.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert\norg.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert\norg.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert\norg.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert\norg.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert\norg.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert\norg.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert\norg.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert\norg.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert\norg.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert\norg.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert\norg.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert\norg.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert\norg.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert\norg.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert\norg.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert\norg.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert\norg.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert\norg.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert\norg.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert\norg.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert\norg.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert\norg.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert\norg.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert\norg.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert\norg.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert\norg.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert\norg.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert\norg.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert\norg.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert\norg.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert\norg.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert\norg.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert\norg.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert\norg.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert\norg.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert\norg.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert\norg.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert\norg.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert\norg.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert\norg.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert\norg.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert\norg.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert\norg.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert\norg.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert\norg.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert\norg.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert\norg.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert\norg.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert\norg.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert\norg.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert\norg.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert\norg.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert\norg.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert\norg.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert\norg.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert\norg.eclipse.jdt.core.formatter.join_lines_in_comments=true\norg.eclipse.jdt.core.formatter.join_wrapped_lines=true\norg.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false\norg.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false\norg.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false\norg.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false\norg.eclipse.jdt.core.formatter.lineSplit=120\norg.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false\norg.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false\norg.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0\norg.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1\norg.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true\norg.eclipse.jdt.core.formatter.tabulation.char=space\norg.eclipse.jdt.core.formatter.tabulation.size=4\norg.eclipse.jdt.core.formatter.use_on_off_tags=false\norg.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false\norg.eclipse.jdt.core.formatter.wrap_before_binary_operator=true\norg.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true\norg.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Mon Feb 22 15:12:15 PST 2016\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-2.11-bin.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": ""
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\ngoto execute\r\n\r\n:4NT_args\r\n@rem Get arguments from the 4NT Shell from JP Software\r\nset CMD_LINE_ARGS=%$\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "installViaTravis.sh",
    "content": "#!/bin/bash\n# This script will build the project.\n\nif [ \"$TRAVIS_PULL_REQUEST\" != \"false\" ]; then\n  echo -e \"Assemble Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]\"\n  ./gradlew clean assemble --stacktrace\nelif [ \"$TRAVIS_PULL_REQUEST\" == \"false\" ] && [ \"$TRAVIS_TAG\" == \"\" ]; then\n  echo -e 'Assemble Branch with Snapshot => Branch ['$TRAVIS_BRANCH']'\n  ./gradlew -Prelease.travisci=true assemble \nelif [ \"$TRAVIS_PULL_REQUEST\" == \"false\" ] && [ \"$TRAVIS_TAG\" != \"\" ]; then\n  echo -e 'Assemble Branch for Release => Branch ['$TRAVIS_BRANCH']  Tag ['$TRAVIS_TAG']'\n  ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true assemble\nelse\n  echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH']  Tag ['$TRAVIS_TAG']  Pull Request ['$TRAVIS_PULL_REQUEST']'\n  ./gradlew assemble\nfi\n"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name='simianarmy'\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/AbstractEmailBuilder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/** The abstract email builder. */\npublic abstract class AbstractEmailBuilder implements EmailBuilder {\n\n    @Override\n    public String buildEmailBody(String emailAddress) {\n        StringBuilder body = new StringBuilder();\n        String header = getHeader();\n        if (header != null) {\n            body.append(header);\n        }\n        String entryTable = getEntryTable(emailAddress);\n        if (entryTable != null) {\n            body.append(entryTable);\n        }\n        String footer = getFooter();\n        if (footer != null) {\n            body.append(footer);\n        }\n        return body.toString();\n    }\n\n    /**\n     * Gets the header to the email body.\n     */\n    protected abstract String getHeader();\n\n    /**\n     * Gets the table of entries in the email body.\n     * @param emailAddress the email address to notify\n     * @return the HTML string representing the table for the resources to send to the\n     * email address\n     */\n    protected abstract String getEntryTable(String emailAddress);\n\n    /**\n     * Gets the footer of the email body.\n     */\n    protected abstract String getFooter();\n\n    /**\n     * Gets the HTML cell in the table of a string value.\n     * @param value the string to put in the table\n     * @return the HTML text\n     */\n    protected String getHtmlCell(String value) {\n        return \"<td style=\\\"padding: 4px\\\">\" + value + \"</td>\";\n    }\n\n    /**\n     * Gets the HTML string displaying the table header with the specified column names.\n     * @param columns the column names for the table\n     */\n    protected String getHtmlTableHeader(String[] columns) {\n        StringBuilder tableHeader = new StringBuilder();\n        tableHeader.append(\n                \"<table border=\\\"1\\\" style=\\\"border-width:1px; border-spacing: 0px; border-collapse: seperate;\\\">\");\n        tableHeader.append(\"<tr style=\\\"background-color: #E8E8E8;\\\" >\");\n        for (String col : columns) {\n            tableHeader.append(getHtmlCell(col));\n        }\n        tableHeader.append(\"</tr>\");\n        return tableHeader.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/CloudClient.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\nimport org.jclouds.compute.ComputeService;\nimport org.jclouds.domain.LoginCredentials;\nimport org.jclouds.ssh.SshClient;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The CloudClient interface. This abstractions provides the interface that the monkeys need to interact with\n * \"the cloud\".\n */\npublic interface CloudClient {\n\n    /**\n     * Terminates instance.\n     *\n     * @param instanceId\n     *            the instance id\n     *\n     * @throws NotFoundException\n     *             if the instance no longer exists or was already terminated after the crawler discovered it then you\n     *             should get a NotFoundException\n     */\n    void terminateInstance(String instanceId);\n\n    /**\n     * Deletes an auto scaling group.\n     *\n     * @param asgName\n     *          the auto scaling group name\n     */\n    void deleteAutoScalingGroup(String asgName);\n\n    /**\n     * Deletes a launch configuration.\n     *\n     * @param launchConfigName\n     *          the launch configuration name\n     */\n    void deleteLaunchConfiguration(String launchConfigName);\n\n    /**\n     * Deletes a volume.\n     *\n     * @param volumeId\n     *          the volume id\n     */\n    void deleteVolume(String volumeId);\n\n    /**\n     * Deletes a snapshot.\n     *\n     * @param snapshotId\n     *          the snapshot id.\n     */\n    void deleteSnapshot(String snapshotId);\n\n    /** Deletes an image.\n     *\n     * @param imageId\n     *          the image id.\n     */\n     void deleteImage(String imageId);\n\n    /**\n     * Deletes an elastic load balancer.\n     *\n     * @param elbId\n     *          the elastic load balancer id\n     */\n    void deleteElasticLoadBalancer(String elbId);\n\n    /**\n     * Deletes a DNS record.\n     *\n     * @param dnsName\n     *          the DNS record to delete\n     * @param dnsType\n     *          the DNS type (CNAME, A, or AAAA)\n     * @param hostedZoneID\n     *          the ID of the hosted zone (required for AWS Route53 records)\n     */\n    public void deleteDNSRecord(String dnsName, String dnsType, String hostedZoneID);\n\n     /**\n     * Adds or overwrites tags for the specified resources.\n     *\n     * @param keyValueMap\n     *          the new tags in the form of map from key to value\n     *\n     * @param resourceIds\n     *          the list of resource ids\n     */\n    void createTagsForResources(Map<String, String> keyValueMap, String... resourceIds);\n\n    /**\n     * Lists all EBS volumes attached to the specified instance.\n     *\n     * @param instanceId\n     *            the instance id\n     * @param includeRoot\n     *            if the root volume is on EBS, should we include it?\n     *\n     * @throws NotFoundException\n     *             if the instance no longer exists or was already terminated after the crawler discovered it then you\n     *             should get a NotFoundException\n     */\n    List<String> listAttachedVolumes(String instanceId, boolean includeRoot);\n\n    /**\n     * Detaches an EBS volumes from the specified instance.\n     *\n     * @param instanceId\n     *            the instance id\n     * @param volumeId\n     *            the volume id\n     * @param force\n     *            if we should force-detach the volume.  Probably best not to use on high-value volumes.\n     *\n     * @throws NotFoundException\n     *             if the instance no longer exists or was already terminated after the crawler discovered it then you\n     *             should get a NotFoundException\n     */\n    void detachVolume(String instanceId, String volumeId, boolean force);\n\n    /**\n     * Returns the jClouds compute service.\n     */\n    ComputeService getJcloudsComputeService();\n\n    /**\n     * Returns the jClouds node id for an instance id on this CloudClient.\n     */\n    String getJcloudsId(String instanceId);\n\n    /**\n     * Opens an SSH connection to an instance.\n     *\n     * @param instanceId\n     *            instance id to connect to\n     * @param credentials\n     *            SSH credentials to use\n     * @return {@link SshClient}, in connected state\n     */\n    SshClient connectSsh(String instanceId, LoginCredentials credentials);\n\n    /**\n     * Finds a security group with the given name, that can be applied to the given instance.\n     *\n     * For example, if it is a VPC instance, it makes sure that it is in the same VPC group.\n     *\n     * @param instanceId\n     *            the instance that the group must be applied to\n     * @param groupName\n     *            the name of the group to find\n     *\n     * @return The group id, or null if not found\n     */\n    String findSecurityGroup(String instanceId, String groupName);\n\n    /**\n     * Creates an (empty) security group, that can be applied to the given instance.\n     *\n     * @param instanceId\n     *            instance that group should be applicable to\n     * @param groupName\n     *            name for new group\n     * @param description\n     *            description for new group\n     *\n     * @return the id of the security group\n     */\n    String createSecurityGroup(String instanceId, String groupName, String description);\n\n    /**\n     * Checks if we can change the security groups of an instance.\n     *\n     * @param instanceId\n     *            instance to check\n     *\n     * @return true iff we can change security groups.\n     */\n    boolean canChangeInstanceSecurityGroups(String instanceId);\n\n    /**\n     * Sets the security groups for an instance.\n     *\n     * Note this is only valid for VPC instances.\n     *\n     * @param instanceId\n     *            the instance id\n     * @param groupIds\n     *            ids of desired new groups\n     *\n     * @throws NotFoundException\n     *             if the instance no longer exists or was already terminated after the crawler discovered it then you\n     *             should get a NotFoundException\n     */\n    void setInstanceSecurityGroups(String instanceId, List<String> groupIds);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/EmailBuilder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/** Interface for build the email body. */\npublic interface EmailBuilder {\n    /**\n     * Builds an email body for an email address.\n     * @param emailAddress the email address to send notification to\n     * @return the email body\n     */\n    String buildEmailBody(String emailAddress);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/EventType.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/**\n * Marker interface for all event type enumerations.\n */\npublic interface EventType extends NamedType {\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/FeatureNotEnabledException.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy;\n\n/**\n * The Class FeatureNotEnabledException.\n *\n * These exceptions will be thrown when a feature is not enabled when being accessed.\n */\npublic class FeatureNotEnabledException extends Exception {\n\n    private static final long serialVersionUID = 8392434473284901306L;\n\n    /**\n     * Instantiates a FeatureNotEnabledException with a message.\n     * @param msg the error message\n     */\n    public FeatureNotEnabledException(String msg) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/GroupType.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/**\n * Marker interface for all group type enumerations.\n */\npublic interface GroupType extends NamedType {\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/InstanceGroupNotFoundException.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy;\n\n/**\n * The Class InstanceGroupNotFoundException.\n *\n * These exceptions will be thrown when an instance group cannot be found with the\n * given name and type.\n */\npublic class InstanceGroupNotFoundException extends Exception {\n\n    private static final long serialVersionUID = -5492120875166280476L;\n\n    private final String groupType;\n    private final String groupName;\n\n    /**\n     * Instantiates an InstanceGroupNotFoundException with the group type and name.\n     * @param groupType the group type\n     * @param groupName the gruop name\n     */\n    public InstanceGroupNotFoundException(String groupType, String groupName) {\n        super(errorMessage(groupType, groupName));\n        this.groupType = groupType;\n        this.groupName = groupName;\n    }\n\n    @Override\n    public String toString() {\n        return errorMessage(groupType, groupName);\n    }\n\n    private static String errorMessage(String groupType, String groupName) {\n        return String.format(\"Instance group named '%s' [type %s] cannot be found.\",\n                groupName, groupType);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/Monkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\n\n/**\n * The abstract Monkey class, it provides a minimal interface from which all monkeys must be derived.\n */\npublic abstract class Monkey {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(Monkey.class);\n\n    /**\n     * The Interface Context.\n     */\n    public interface Context {\n\n        /**\n         * Scheduler.\n         *\n         * @return the monkey scheduler\n         */\n        MonkeyScheduler scheduler();\n\n        /**\n         * Calendar.\n         *\n         * @return the monkey calendar\n         */\n        MonkeyCalendar calendar();\n\n        /**\n         * Cloud client.\n         *\n         * @return the cloud client\n         */\n        CloudClient cloudClient();\n\n        /**\n         * Recorder.\n         *\n         * @return the monkey recorder\n         */\n        MonkeyRecorder recorder();\n\n        /**\n         * Add a event to the summary report. The ChaosMonkey uses this to print a summary after the chaos run is\n         * complete.\n         *\n         * @param evt\n         *            The Event to be reported\n         */\n        void reportEvent(Event evt);\n\n        /**\n         * Used to clear the event summary on the start of a chaos run.\n         */\n        void resetEventReport();\n\n        /**\n         * Returns a summary of what the chaos run did.\n         */\n        String getEventReport();\n\n        /**\n         * Configuration.\n         *\n         * @return the monkey configuration\n         */\n        MonkeyConfiguration configuration();\n    }\n\n    /** The context. */\n    private final Context ctx;\n\n    /**\n     * Instantiates a new monkey.\n     *\n     * @param ctx\n     *            the context\n     */\n    public Monkey(Context ctx) {\n        this.ctx = ctx;\n    }\n\n    /**\n     * Type.\n     *\n     * @return the monkey type enum\n     */\n    public abstract MonkeyType type();\n\n    /**\n     * Do monkey business.\n     */\n    public abstract void doMonkeyBusiness();\n\n    /**\n     * Context.\n     *\n     * @return the context\n     */\n    public Context context() {\n        return ctx;\n    }\n\n    /**\n     * Run. This is run on the schedule set by the MonkeyScheduler\n     */\n    public void run() {\n        if (ctx.calendar().isMonkeyTime(this)) {\n            LOGGER.info(this.type().name() + \" Monkey Running ...\");\n            try {\n                this.doMonkeyBusiness();\n            } finally {\n                String eventReport = context().getEventReport();\n                if (eventReport != null) {\n                    LOGGER.info(\"Reporting what I did...\\n\" + eventReport);\n                }\n            }\n        } else {\n            LOGGER.info(\"Not Time for \" + this.type().name() + \" Monkey\");\n        }\n    }\n\n    /**\n     * Start. Sets up the schedule for the monkey to run on.\n     */\n    public void start() {\n        final Monkey me = this;\n        ctx.scheduler().start(this, new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    me.run();\n                } catch (Exception e) {\n                    LOGGER.error(me.type().name() + \" Monkey Error: \", e);\n                }\n            }\n        });\n    }\n\n    /**\n     * Stop. Removes the monkey from the schedule.\n     */\n    public void stop() {\n        ctx.scheduler().stop(this);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/MonkeyCalendar.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * The Interface MonkeyCalendar used to tell if a monkey should be running or now. We only want monkeys to run during\n * business hours, so that engineers will be on-hand if something goes wrong.\n */\npublic interface MonkeyCalendar {\n\n    /**\n     * Checks if is monkey time.\n     *\n     * @param monkey\n     *            the monkey\n     * @return true, if is monkey time\n     */\n    boolean isMonkeyTime(Monkey monkey);\n\n    /**\n     * Open hour. This is the \"open\" hour for then the monkey should start working.\n     *\n     * @return the int\n     */\n    int openHour();\n\n    /**\n     * Close hour. This is the \"close\" hour for when the monkey should stop working.\n     *\n     * @return the int\n     */\n    int closeHour();\n\n    /**\n     * Get the current time using whatever timezone is used for monkey date calculations.\n     *\n     * @return the calendar\n     */\n    Calendar now();\n\n    /** Gets the next business day from the start date after n business days.\n     *\n     * @param date the start date\n     * @param n the number of business days from now\n     * @return the business day after n business days\n     */\n    Date getBusinessDay(Date date, int n);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/MonkeyConfiguration.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/**\n * The Interface MonkeyConfiguration.\n */\npublic interface MonkeyConfiguration {\n\n    /**\n     * Gets the boolean associated with property string. If not found it will return false.\n     *\n     * @param property\n     *            the property name\n     * @return the boolean value\n     */\n    boolean getBool(String property);\n\n    /**\n     * Gets the boolean associated with property string. If not found it will return dflt.\n     *\n     * @param property\n     *            the property name\n     * @param dflt\n     *            the default value\n     * @return the bool property value, or dflt if none set\n     */\n    boolean getBoolOrElse(String property, boolean dflt);\n\n    /**\n     * Gets the number (double) associated with property string. If not found it will return dflt.\n     *\n     * @param property\n     *            the property name\n     * @param dflt\n     *            the default value\n     * @return the numeric property value, or dflt if none set\n     */\n    double getNumOrElse(String property, double dflt);\n\n    /**\n     * Gets the string associated with property string. If not found it will return null.\n     *\n     * @param property\n     *            the property name\n     * @return the string property value\n     */\n    String getStr(String property);\n\n    /**\n     * Gets the string associated with property string. If not found it will return dflt.\n     *\n     * @param property\n     *            the property name\n     * @param dflt\n     *            the default value\n     * @return the string property value, or dflt if none set\n     */\n    String getStrOrElse(String property, String dflt);\n\n    /**\n     * If the configuration has dynamic elements then they should be reloaded with this.\n     */\n    void reload();\n\n    /**\n     * Reloads the properties of specific group.\n     * @param groupName\n     *          the instance group's name\n     */\n    void reload(String groupName);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/MonkeyEmailNotifier.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n\n/** The interface for the email notifier used by monkeys. */\npublic interface MonkeyEmailNotifier {\n\n    /**\n     * Determines if a email address is valid.\n     * @param email the email\n     * @return true if the email address is valid, false otherwise.\n     */\n    boolean isValidEmail(String email);\n\n    /**\n     * Builds an email subject for an email address.\n     * @param to the destination email address\n     * @return the email subject\n     */\n    String buildEmailSubject(String to);\n\n    /**\n     * Gets the cc email addresses for a to address.\n     * @param to the to address\n     * @return the cc email addresses\n     */\n    String[] getCcAddresses(String to);\n\n    /**\n     * Gets the source email addresses for a to address.\n     * @param to the to address\n     * @return the source email addresses\n     */\n    String getSourceAddress(String to);\n\n    /**\n     * Sends an email.\n     * @param to the address sent to\n     * @param subject the email subject\n     * @param body the email body\n     */\n    void sendEmail(String to, String subject, String body);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/MonkeyRecorder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The Interface MonkeyRecorder. This is use to store and find events in some datastore.\n */\npublic interface MonkeyRecorder {\n\n    /**\n     * The Interface Event.\n     */\n    public interface Event {\n\n        /**\n         * Event Id.\n         *\n         * @return the string\n         */\n        String id();\n\n        /**\n         * Event time.\n         *\n         * @return the date\n         */\n        Date eventTime();\n\n        /**\n         * Monkey type.\n         *\n         * @return the monkey type enum\n         */\n        MonkeyType monkeyType();\n\n        /**\n         * Event type.\n         *\n         * @return the event type enum\n         */\n        EventType eventType();\n\n        /**\n         * Region.\n         *\n         * @return the region for the event\n         */\n        String region();\n\n        /**\n         * Fields.\n         *\n         *\n         * @return the map of strings that may have been provided when the event was created\n         */\n        Map<String, String> fields();\n\n        /**\n         * Field.\n         *\n         * @param name\n         *            the name\n         * @return the string associated with that field\n         */\n        String field(String name);\n\n        /**\n         * Adds the field.\n         *\n         * @param name\n         *            the name\n         * @param value\n         *            the value\n         * @return <b>this</b> so you can chain multiple addField calls together\n         */\n        Event addField(String name, String value);\n    }\n\n    /**\n     * New event.\n     *\n     * @param monkeyType\n     *            the monkey type\n     * @param eventType\n     *            the event type\n     * @param region\n     *            the region the event occurred\n     * @param id\n     *            the id\n     * @return the event\n     */\n    Event newEvent(MonkeyType monkeyType, EventType eventType, String region, String id);\n\n    default Event newEvent(MonkeyType monkeyType, EventType eventType, Resource resource, String id) {\n        if (resource == null) throw new IllegalArgumentException(\"resource must not be null\");\n        Event event = newEvent(monkeyType, eventType, resource.getRegion(), id);\n        if (resource.getAllTagKeys() != null) {\n            for(String key : resource.getAllTagKeys()) {\n                event.addField(key, resource.getTag(key));\n            }\n        }\n        event.addField(\"ResourceDescription\", resource.getDescription());\n        event.addField(\"ResourceType\", resource.getResourceType().toString());\n        event.addField(\"ResourceId\", resource.getId());\n        return event;\n    }\n\n    /**\n     * Record event.\n     *\n     * @param evt\n     *            the evt\n     */\n    void recordEvent(Event evt);\n\n    /**\n     * Find events.\n     *\n     * @param query\n     *            arbitrary map of strings to used to filter the results\n     * @param after\n     *            the after\n     * @return the list of events\n     */\n    List<Event> findEvents(Map<String, String> query, Date after);\n\n    /**\n     * Find events.\n     *\n     * @param monkeyType\n     *            the monkey type\n     * @param query\n     *            arbitrary map of strings to used to filter the results\n     * @param after\n     *            the after\n     * @return the list of events\n     */\n    List<Event> findEvents(MonkeyType monkeyType, Map<String, String> query, Date after);\n\n    /**\n     * Find events.\n     *\n     * @param monkeyType\n     *            the monkey type\n     * @param eventType\n     *            the event type\n     * @param query\n     *            arbitrary map of strings to used to filter the results\n     * @param after\n     *            the after\n     * @return the list\n     */\n    List<Event> findEvents(MonkeyType monkeyType, EventType eventType, Map<String, String> query, Date after);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/MonkeyRunner.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\nimport java.lang.reflect.Constructor;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.ListIterator;\nimport java.util.Map;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * The MonkeyRunner Singleton.\n */\npublic enum MonkeyRunner {\n\n    /** The instance. */\n    INSTANCE;\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(MonkeyRunner.class);\n\n    /**\n     * Gets the single instance of MonkeyRunner.\n     *\n     * @return single instance of MonkeyRunner\n     */\n    public static MonkeyRunner getInstance() {\n        return INSTANCE;\n    }\n\n    /**\n     * Start all the monkeys registered with addMonkey or replaceMonkey.\n     */\n    public void start() {\n        for (Monkey monkey : monkeys) {\n            LOGGER.info(\"Starting \" + monkey.type().name() + \" Monkey\");\n            monkey.start();\n        }\n    }\n\n    /**\n     * Stop all of the registered monkeys.\n     */\n    public void stop() {\n        for (Monkey monkey : monkeys) {\n            LOGGER.info(\"Stopping \" + monkey.type().name() + \" Monkey\");\n            monkey.stop();\n        }\n    }\n\n    /**\n     * The monkey map. Maps the monkey class to the context class that is registered. This is so we can create new\n     * monkeys in factory() that have the same context types as the registered ones.\n     */\n    private final Map<Class<? extends Monkey>, Class<? extends Monkey.Context>> monkeyMap =\n            new HashMap<Class<? extends Monkey>, Class<? extends Monkey.Context>>();\n\n    /** The monkeys. */\n    private final List<Monkey> monkeys = new LinkedList<Monkey>();\n\n    /**\n     * Gets the registered monkeys.\n     *\n     * @return the monkeys\n     */\n    public List<Monkey> getMonkeys() {\n        return Collections.unmodifiableList(monkeys);\n    }\n\n    /**\n     * Adds a simple monkey void constructor.\n     *\n     * @param monkeyClass\n     *            the monkey class\n     */\n    public void addMonkey(Class<? extends Monkey> monkeyClass) {\n        addMonkey(monkeyClass, null);\n    }\n\n    /**\n     * Replace a simple monkey that has void constructor.\n     *\n     * @param monkeyClass\n     *            the monkey class\n     */\n    public void replaceMonkey(Class<? extends Monkey> monkeyClass) {\n        replaceMonkey(monkeyClass, null);\n    }\n\n    /**\n     * Adds the monkey.\n     *\n     * @param monkeyClass\n     *            the monkey class\n     * @param ctxClass\n     *            the context class that is passed to the monkey class constructor.\n     */\n    public void addMonkey(Class<? extends Monkey> monkeyClass, Class<? extends Monkey.Context> ctxClass) {\n        if (monkeyMap.containsKey(monkeyClass)) {\n            throw new RuntimeException(monkeyClass.getName()\n                    + \" already registered, use replaceMonkey instead of addMonkey\");\n        }\n        monkeyMap.put(monkeyClass, ctxClass);\n        monkeys.add(factory(monkeyClass, ctxClass));\n    }\n\n    /**\n     * Replace monkey. If a monkey is already registered this will replace that registered monkey.\n     *\n     * @param monkeyClass\n     *            the monkey class\n     * @param ctxClass\n     *            the context class that is passed to the monkey class constructor.\n     */\n    public void replaceMonkey(Class<? extends Monkey> monkeyClass, Class<? extends Monkey.Context> ctxClass) {\n        monkeyMap.put(monkeyClass, ctxClass);\n        ListIterator<Monkey> li = monkeys.listIterator();\n        while (li.hasNext()) {\n            Monkey monkey = li.next();\n            if (monkey.getClass() == monkeyClass) {\n                li.set(factory(monkeyClass, ctxClass));\n                return;\n            }\n        }\n        Monkey monkey = factory(monkeyClass, ctxClass);\n        monkeys.add(monkey);\n    }\n\n    /**\n     * Removes the monkey. factory() will no longer be able to construct monkeys of the specified monkey class.\n     *\n     * @param monkeyClass\n     *            the monkey class\n     */\n    public void removeMonkey(Class<? extends Monkey> monkeyClass) {\n        ListIterator<Monkey> li = monkeys.listIterator();\n        while (li.hasNext()) {\n            Monkey monkey = li.next();\n            if (monkey.getClass() == monkeyClass) {\n                monkey.stop();\n                li.remove();\n                break;\n            }\n        }\n        monkeyMap.remove(monkeyClass);\n    }\n\n    /**\n     * Monkey factory. This will generate a new monkey object of the monkeyClass type. If a monkey of monkeyClass has\n     * not been registered then this will attempt to find a registered subclass and create an object of that type.\n     * Example:\n     *\n     * <pre>\n     *         {@code\n     *         MonkeyRunner.getInstance().addMonkey(BasicChaosMonkey.class, BasicMonkeyContext.class);\n     *         // This will actually return a BasicChaosMonkey since that is the only subclass that was registered\n     *         ChaosMonkey monkey = MonkeyRunner.getInstance().factory(ChaosMonkey.class);\n     *}\n     * </pre>\n     *\n     * @param <T>\n     *            the generic type, must be a subclass of Monkey\n     * @param monkeyClass\n     *            the monkey class\n     * @return the monkey\n     */\n    public <T extends Monkey> T factory(Class<T> monkeyClass) {\n        Class<? extends Monkey.Context> ctxClass = getContextClass(monkeyClass);\n        if (ctxClass == null) {\n            // look for derived class already in our map\n            for (Map.Entry<Class<? extends Monkey>, Class<? extends Monkey.Context>> pair : monkeyMap.entrySet()) {\n                if (monkeyClass.isAssignableFrom(pair.getKey())) {\n                    @SuppressWarnings(\"unchecked\")\n                    T monkey = (T) factory(pair.getKey(), pair.getValue());\n                    return monkey;\n                }\n            }\n        }\n        return factory(monkeyClass, ctxClass);\n    }\n\n    /**\n     * Monkey Factory. Given a monkey class and a monkey context class it will generate a new monkey. If the\n     * contextClass is null it will try to generate a new monkeyClass with a void constructor;\n     *\n     * @param <T>\n     *            the generic type, must be a subclass of Monkey\n     * @param monkeyClass\n     *            the monkey class\n     * @param contextClass\n     *            the context class\n     * @return the monkey\n     */\n    public <T extends Monkey> T factory(Class<T> monkeyClass, Class<? extends Monkey.Context> contextClass) {\n        try {\n            if (contextClass == null) {\n                // assume Monkey class has has void ctor\n                return monkeyClass.newInstance();\n            }\n\n            // then find corresponding ctor\n            for (Constructor<?> ctor : monkeyClass.getDeclaredConstructors()) {\n                Class<?>[] paramTypes = ctor.getParameterTypes();\n                if (paramTypes.length != 1) {\n                    continue;\n                }\n                if (paramTypes[0].getName().endsWith(\"$Context\")) {\n                    @SuppressWarnings(\"unchecked\")\n                    T monkey = (T) ctor.newInstance(contextClass.newInstance());\n                    return monkey;\n                }\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"monkeyFactory error, cannot make monkey from \" + monkeyClass.getName() + \" with \"\n                    + (contextClass == null ? null : contextClass.getName()), e);\n        }\n\n        return null;\n    }\n\n    /**\n     * Gets the context class. You should not need this.\n     *\n     * @param monkeyClass\n     *            the monkey class\n     * @return the context class or null if a monkeyClass has not been registered\n     */\n    public Class<? extends Monkey.Context> getContextClass(Class<? extends Monkey> monkeyClass) {\n        return monkeyMap.get(monkeyClass);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/MonkeyScheduler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Interface MonkeyScheduler.\n */\npublic interface MonkeyScheduler {\n\n    /**\n     * Frequency. How often the monkey should run, works in conjunction with frequencyUnit(). If frequency is 2 and\n     * frequencyUnit is TimeUnit.HOUR then the monkey will run once ever 2 hours.\n     *\n     * @return the frequency interval\n     */\n    int frequency();\n\n    /**\n     * Frequency unit. This is the time unit that corresponds with frequency().\n     *\n     * @return time unit\n     */\n    TimeUnit frequencyUnit();\n\n    /**\n     * Start the scheduler to cause the monkey run at a specified interval.\n     *\n     * @param monkey\n     *            the monkey being scheduled\n     * @param run\n     *            the Runnable to start, generally calls doMonkeyBusiness\n     */\n    void start(Monkey monkey, Runnable run);\n\n    /**\n     * Stop the scheduler for a given monkey. After this the monkey will no longer run on the fixed schedule.\n     *\n     * @param monkey\n     *            the monkey being scheduled\n     */\n    void stop(Monkey monkey);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/MonkeyType.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/**\n * Marker interface for all monkey type enumerations.\n */\npublic interface MonkeyType extends NamedType {\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/NamedType.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/**\n * Interface requiring a name() method.\n */\npublic interface NamedType {\n\n    /**\n     * Name of this instance.\n     */\n    String name();\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/NotFoundException.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/**\n * The Class NotFoundException.\n *\n * These exceptions will be thrown when a Monkey is trying to interact with a remote resource but it no longer exists\n * (or never existed). It is used as an adapter to translate a cloud provider exception into something common that the\n * monkeys can easily handle.\n */\n@SuppressWarnings(\"serial\")\npublic class NotFoundException extends RuntimeException {\n\n    /**\n     * Instantiates a new NotFound exception.\n     *\n     * @param message\n     *            the exception message\n     */\n    public NotFoundException(String message) {\n        super(message);\n    }\n\n    /**\n     * Instantiates a new NotFound exception.\n     *\n     * @param message\n     *            the exception message\n     * @param cause\n     *            the exception cause. This should be the raw exception from the cloud provider.\n     */\n    public NotFoundException(String message, Throwable cause) {\n        super(message, cause);\n    }\n\n    /**\n     * Instantiates a new NotFound exception.\n     *\n     * @param cause\n     *            the exception cause. This should be the raw exception from the cloud provider.\n     */\n    public NotFoundException(Throwable cause) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/Resource.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy;\n\nimport java.util.Collection;\nimport java.util.Date;\nimport java.util.Map;\n\n/**\n * The interface of Resource. It defines the interfaces for getting the common properties of a resource, as well as\n * the methods to add and retrieve the additional properties of a resource. Instead of defining a new subclass of\n * the Resource interface, new resources that have additional fields other than the common ones can be represented,\n * by adding field-value pairs. This approach makes serialization and deserialization of resources much easier with\n * the cost of type safety.\n */\npublic interface Resource {\n    /** The enum representing the cleanup state of a resource. **/\n    public enum CleanupState {\n        /** The resource is marked as a cleanup candidate but has not been cleaned up yet. **/\n        MARKED,\n        /** The resource is terminated by janitor monkey. **/\n        JANITOR_TERMINATED,\n        /** The resource is terminated by user before janitor monkey performs the termination. **/\n        USER_TERMINATED,\n        /** The resource is unmarked and not for cleanup anymore due to some change of situations. **/\n        UNMARKED\n    }\n\n    /**\n     * Gets the resource id.\n     *\n     * @return the resource id\n     */\n    String getId();\n\n    /**\n     * Sets the resource id.\n     *\n     * @param id the resource id\n     */\n    void setId(String id);\n\n    /**\n     * Sets the resource id and returns the resource.\n     *\n     * @param id the resource id\n     * @return the resource object\n     */\n    Resource withId(String id);\n\n    /**\n     * Gets the resource type.\n     *\n     * @return the resource type enum\n     */\n    ResourceType getResourceType();\n\n    /**\n     * Sets the resource type.\n     *\n     * @param type the resource type enum\n     */\n    void setResourceType(ResourceType type);\n\n    /**\n     * Sets the resource type and returns the resource.\n     *\n     * @param type resource type enum\n     * @return the resource object\n     */\n    Resource withResourceType(ResourceType type);\n\n    /**\n     * Gets the region the resource is in.\n     *\n     * @return the region of the resource\n     */\n    String getRegion();\n\n    /**\n     * Sets the region the resource is in.\n     *\n     * @param region the region the resource is in\n     */\n    void setRegion(String region);\n\n    /**\n     * Sets the resource region and returns the resource.\n     *\n     * @param region the region the resource is in\n     * @return the resource object\n     */\n    Resource withRegion(String region);\n\n    /**\n     * Gets the owner email of the resource.\n     *\n     * @return the owner email of the resource\n     */\n    String getOwnerEmail();\n\n    /**\n     * Sets the owner email of the resource.\n     *\n     * @param ownerEmail the owner email of the resource\n     */\n    void setOwnerEmail(String ownerEmail);\n\n    /**\n     * Sets the resource owner email and returns the resource.\n     *\n     * @param ownerEmail the owner email of the resource\n     * @return the resource object\n     */\n    Resource withOwnerEmail(String ownerEmail);\n\n    /**\n     * Gets the description of the resource.\n     *\n     * @return the description of the resource\n     */\n    String getDescription();\n\n    /**\n     * Sets the description of the resource.\n     *\n     * @param description the description of the resource\n     */\n    void setDescription(String description);\n\n    /**\n     * Sets the resource description and returns the resource.\n     *\n     * @param description the description of the resource\n     * @return the resource object\n     */\n    Resource withDescription(String description);\n\n    /**\n     * Gets the launch time of the resource.\n     *\n     * @return the launch time of the resource\n     */\n    Date getLaunchTime();\n\n    /**\n     * Sets the launch time of the resource.\n     *\n     * @param launchTime the launch time of the resource\n     */\n    void setLaunchTime(Date launchTime);\n\n    /**\n     * Sets the resource launch time and returns the resource.\n     *\n     * @param launchTime the launch time of the resource\n     * @return the resource object\n     */\n    Resource withLaunchTime(Date launchTime);\n\n    /**\n     * Gets the time that when the resource is marked as a cleanup candidate.\n     *\n     * @return the time that when the resource is marked as a cleanup candidate\n     */\n    Date getMarkTime();\n\n    /**\n     * Sets the time that when the resource is marked as a cleanup candidate.\n     *\n     * @param markTime the time that when the resource is marked as a cleanup candidate\n     */\n    void setMarkTime(Date markTime);\n\n    /**\n     * Sets the resource mark time and returns the resource.\n     *\n     * @param markTime the time that when the resource is marked as a cleanup candidate\n     * @return the resource object\n     */\n    Resource withMarkTime(Date markTime);\n\n    /**\n     * Gets the the time that when the resource is expected to be terminated.\n     *\n     * @return the time that when the resource is expected to be terminated\n     */\n    Date getExpectedTerminationTime();\n\n    /**\n     * Sets the time that when the resource is expected to be terminated.\n     *\n     * @param expectedTerminationTime the time that when the resource is expected to be terminated\n     */\n    void setExpectedTerminationTime(Date expectedTerminationTime);\n\n    /**\n     * Sets the time that when the resource is expected to be terminated and returns the resource.\n     *\n     * @param expectedTerminationTime the time that when the resource is expected to be terminated\n     * @return the resource object\n     */\n    Resource withExpectedTerminationTime(Date expectedTerminationTime);\n\n    /**\n     * Gets the time that when the resource is actually terminated.\n     *\n     * @return the time that when the resource is actually terminated\n     */\n    Date getActualTerminationTime();\n\n    /**\n     * Sets the time that when the resource is actually terminated.\n     *\n     * @param actualTerminationTime the time that when the resource is actually terminated\n     */\n    void setActualTerminationTime(Date actualTerminationTime);\n\n    /**\n     * Sets the resource actual termination time and returns the resource.\n     *\n     * @param actualTerminationTime the time that when the resource is actually terminated\n     * @return the resource object\n     */\n    Resource withActualTerminationTime(Date actualTerminationTime);\n\n    /**\n     * Gets the time that when the owner is notified about the cleanup of the resource.\n     *\n     * @return the time that when the owner is notified about the cleanup of the resource\n     */\n    Date getNotificationTime();\n\n    /**\n     * Sets the time that when the owner is notified about the cleanup of the resource.\n     *\n     * @param notificationTime the time that when the owner is notified about the cleanup of the resource\n     */\n    void setNotificationTime(Date notificationTime);\n\n    /**\n     * Sets the time that when the owner is notified about the cleanup of the resource and returns the resource.\n     *\n     * @param notificationTime the time that when the owner is notified about the cleanup of the resource\n     * @return the resource object\n     */\n    Resource withNnotificationTime(Date notificationTime);\n\n    /**\n     * Gets the resource state.\n     *\n     * @return the resource state enum\n     */\n    CleanupState getState();\n\n    /**\n     * Sets the resource state.\n     *\n     * @param state the resource state\n     */\n    void setState(CleanupState state);\n\n    /**\n     * Sets the resource state and returns the resource.\n     *\n     * @param state resource state enum\n     * @return the resource object\n     */\n    Resource withState(CleanupState state);\n\n    /**\n     * Gets the termination reason of the resource.\n     *\n     * @return the termination reason of the resource\n     */\n    String getTerminationReason();\n\n    /**\n     * Sets the termination reason of the resource.\n     *\n     * @param terminationReason the termination reason of the resource\n     */\n    void setTerminationReason(String terminationReason);\n\n    /**\n     * Sets the resource termination reason and returns the resource.\n     *\n     * @param terminationReason the termination reason of the resource\n     * @return the resource object\n     */\n    Resource withTerminationReason(String terminationReason);\n\n    /**\n     * Gets the boolean to indicate whether or not the resource is opted out of Janitor monkey\n     * so it will not be cleaned.\n     * @return true if the resource is opted out of Janitor monkey, otherwise false\n     */\n    boolean isOptOutOfJanitor();\n\n    /**\n     * Sets the flag to indicate whether or not the resource is opted out of Janitor monkey\n     * so it will not be cleaned.\n     * @param optOutOfJanitor true if the resource is opted out of Janitor monkey, otherwise false\n     */\n    void setOptOutOfJanitor(boolean optOutOfJanitor);\n\n    /**\n     * Sets the flag to indicate whether or not the resource is opted out of Janitor monkey\n     * so it will not be cleaned and returns the resource object.\n     * @param optOutOfJanitor true if the resource is opted out of Janitor monkey, otherwise false\n     * @return the resource object\n     */\n    Resource withOptOutOfJanitor(boolean optOutOfJanitor);\n\n    /**\n     * Gets a map from fields of resources to corresponding values. Values are represented\n     * as Strings so they can be displayed or stored in databases like SimpleDB.\n     * @return a map from field name to field value\n     */\n    Map<String, String> getFieldToValueMap();\n\n    /** Adds or sets an additional field with the specified name and value to the resource.\n     *\n     * @param fieldName the field name\n     * @param fieldValue the field value\n     * @return the resource itself for chaining\n     */\n    Resource setAdditionalField(String fieldName, String fieldValue);\n\n    /** Gets the value of an additional field with the specified name of the resource.\n     *\n     * @param fieldName the field name\n     * @return the field value\n     */\n    String getAdditionalField(String fieldName);\n\n    /**\n     * Gets all additional field names in the resource.\n     * @return a collection of names of all additional fields\n     */\n    Collection<String> getAdditionalFieldNames();\n\n    /**\n     * Adds a tag with the specified key and value to the resource.\n     * @param key the key of the tag\n     * @param value the value of the tag\n     */\n    void setTag(String key, String value);\n\n    /**\n     * Gets the tag value for a specific key of the resource.\n     * @param key the key of the tag\n     * @return the value of the tag\n     */\n    String getTag(String key);\n\n    /**\n     * Gets all the keys of tags.\n     * @return collection of keys of all tags\n     */\n    Collection<String> getAllTagKeys();\n\n\n    /** Clone a resource with the exact field values of the current object.\n     *\n     * @return the clone of the resource\n     */\n    Resource cloneResource();\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/ResourceType.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\n/**\n * Marker interface for all resource type enumerations.\n */\npublic interface ResourceType extends NamedType {\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/AWSEmailNotifier.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws;\n\nimport java.util.regex.Pattern;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.amazonaws.services.simpleemail.model.Body;\nimport com.amazonaws.services.simpleemail.model.Content;\nimport com.amazonaws.services.simpleemail.model.Destination;\nimport com.amazonaws.services.simpleemail.model.Message;\nimport com.amazonaws.services.simpleemail.model.SendEmailRequest;\nimport com.amazonaws.services.simpleemail.model.SendEmailResult;\nimport com.netflix.simianarmy.MonkeyEmailNotifier;\n\n/**\n * The class implements the monkey email notifier using AWS simple email service\n * for sending email.\n */\npublic abstract class AWSEmailNotifier implements MonkeyEmailNotifier {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(AWSEmailNotifier.class);\n    private static final String EMAIL_PATTERN =\n            \"^[_A-Za-z0-9-\\\\+\\\\.#]+(.[_A-Za-z0-9-#]+)*@\"\n                    + \"[A-Za-z0-9-]+(\\\\.[A-Za-z0-9]+)*(\\\\.[A-Za-z]{2,})$\";\n\n    private final Pattern emailPattern;\n\n    private final AmazonSimpleEmailServiceClient sesClient;\n\n    /**\n     * The constructor.\n     */\n    public AWSEmailNotifier(AmazonSimpleEmailServiceClient sesClient) {\n        super();\n        this.sesClient = sesClient;\n        this.emailPattern = Pattern.compile(EMAIL_PATTERN);\n    }\n\n    @Override\n    public void sendEmail(String to, String subject, String body) {\n        if (!isValidEmail(to)) {\n            LOGGER.error(String.format(\"The destination email address %s is not valid, no email is sent.\", to));\n            return;\n        }\n        if (sesClient == null) {\n            String msg = \"The email client is not set.\";\n            LOGGER.error(msg);\n            throw new RuntimeException(msg);\n        }\n        Destination destination = new Destination().withToAddresses(to)\n                .withCcAddresses(getCcAddresses(to));\n        Content subjectContent = new Content(subject);\n        Content bodyContent = new Content();\n        Body msgBody = new Body(bodyContent);\n        msgBody.setHtml(new Content(body));\n        Message msg = new Message(subjectContent, msgBody);\n        String sourceAddress = getSourceAddress(to);\n        SendEmailRequest request = new SendEmailRequest(sourceAddress, destination, msg);\n        request.setReturnPath(sourceAddress);\n        LOGGER.debug(String.format(\"Sending email with subject '%s' to %s\",\n                subject, to));\n        SendEmailResult result = null;\n        try {\n            result = sesClient.sendEmail(request);\n        } catch (Exception e) {\n            throw new RuntimeException(String.format(\"Failed to send email to %s\", to), e);\n        }\n        LOGGER.info(String.format(\"Email to %s, result id is %s, subject is %s\",\n                to, result.getMessageId(), subject));\n    }\n\n    @Override\n    public boolean isValidEmail(String email) {\n        if (email == null) {\n            return false;\n        }\n        if (!emailPattern.matcher(email).matches()) {\n            LOGGER.error(String.format(\"Invalid email address: %s\", email));\n            return false;\n        }\n        if (email.equals(\"foo@bar.com\")) {\n            LOGGER.error(String.format(\"Email address not changed from default; treating as invalid: %s\", email));\n            return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/AWSResource.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws;\n\nimport java.util.*;\n\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\n\nimport com.netflix.simianarmy.NamedType;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\n\n/**\n * The class represents general AWS resources that are managed by janitor monkey.\n */\npublic class AWSResource implements Resource {\n    private String id;\n    private ResourceType resourceType;\n    private String region;\n    private String ownerEmail;\n    private String description;\n    private String terminationReason;\n    private CleanupState state;\n    private Date expectedTerminationTime;\n    private Date actualTerminationTime;\n    private Date notificationTime;\n    private Date launchTime;\n    private Date markTime;\n    private boolean optOutOfJanitor;\n    private String awsResourceState;\n\n    /** The field name for resourceId. **/\n    public static final String FIELD_RESOURCE_ID = \"resourceId\";\n    /** The field name for resourceType. **/\n    public static final String FIELD_RESOURCE_TYPE = \"resourceType\";\n    /** The field name for region. **/\n    public static final String FIELD_REGION = \"region\";\n    /** The field name for owner email. **/\n    public static final String FIELD_OWNER_EMAIL = \"ownerEmail\";\n    /** The field name for description. **/\n    public static final String FIELD_DESCRIPTION = \"description\";\n    /** The field name for state. **/\n    public static final String FIELD_STATE = \"state\";\n    /** The field name for terminationReason. **/\n    public static final String FIELD_TERMINATION_REASON = \"terminationReason\";\n    /** The field name for expectedTerminationTime. **/\n    public static final String FIELD_EXPECTED_TERMINATION_TIME = \"expectedTerminationTime\";\n    /** The field name for actualTerminationTime. **/\n    public static final String FIELD_ACTUAL_TERMINATION_TIME = \"actualTerminationTime\";\n    /** The field name for notificationTime. **/\n    public static final String FIELD_NOTIFICATION_TIME = \"notificationTime\";\n    /** The field name for launchTime. **/\n    public static final String FIELD_LAUNCH_TIME = \"launchTime\";\n    /** The field name for markTime. **/\n    public static final String FIELD_MARK_TIME = \"markTime\";\n    /** The field name for isOptOutOfJanitor. **/\n    public static final String FIELD_OPT_OUT_OF_JANITOR = \"optOutOfJanitor\";\n    /** The field name for awsResourceState. **/\n    public static final String FIELD_AWS_RESOURCE_STATE = \"awsResourceState\";\n\n    /** The date format used to print or parse a Date value. **/\n    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern(\"yyyy-MM-dd'T'HH:mm:ss\");\n\n    /** The map from name to value for additional fields used by the resource. **/\n    private final Map<String, String> additionalFields = new HashMap<String, String>();\n\n    /** The map from AWS tag key to value for the resource. **/\n    private final Map<String, String> tags = new HashMap<String, String>();\n\n    /** {@inheritDoc} */\n    @Override\n    public Map<String, String> getFieldToValueMap() {\n        Map<String, String> fieldToValue = new HashMap<String, String>();\n\n        putToMapIfNotNull(fieldToValue, FIELD_RESOURCE_ID, getId());\n        putToMapIfNotNull(fieldToValue, FIELD_RESOURCE_TYPE, getResourceType());\n        putToMapIfNotNull(fieldToValue, FIELD_REGION, getRegion());\n        putToMapIfNotNull(fieldToValue, FIELD_OWNER_EMAIL, getOwnerEmail());\n        putToMapIfNotNull(fieldToValue, FIELD_DESCRIPTION, getDescription());\n        putToMapIfNotNull(fieldToValue, FIELD_STATE, getState());\n        putToMapIfNotNull(fieldToValue, FIELD_TERMINATION_REASON, getTerminationReason());\n        putToMapIfNotNull(fieldToValue, FIELD_EXPECTED_TERMINATION_TIME, printDate(getExpectedTerminationTime()));\n        putToMapIfNotNull(fieldToValue, FIELD_ACTUAL_TERMINATION_TIME, printDate(getActualTerminationTime()));\n        putToMapIfNotNull(fieldToValue, FIELD_NOTIFICATION_TIME, printDate(getNotificationTime()));\n        putToMapIfNotNull(fieldToValue, FIELD_LAUNCH_TIME, printDate(getLaunchTime()));\n        putToMapIfNotNull(fieldToValue, FIELD_MARK_TIME, printDate(getMarkTime()));\n        putToMapIfNotNull(fieldToValue, FIELD_AWS_RESOURCE_STATE, getAWSResourceState());\n\n        // Additional fields are serialized while tags are not. So if any tags need to be\n        // serialized as well, put them to additional fields.\n        fieldToValue.put(FIELD_OPT_OUT_OF_JANITOR, String.valueOf(isOptOutOfJanitor()));\n\n        fieldToValue.putAll(additionalFields);\n\n        return fieldToValue;\n    }\n\n    /**\n     * Parse a map from field name to value to a resource.\n     * @param fieldToValue the map from field name to value\n     * @return the resource that is de-serialized from the map\n     */\n    public static AWSResource parseFieldtoValueMap(Map<String, String> fieldToValue) {\n        AWSResource resource = new AWSResource();\n        for (Map.Entry<String, String> field : fieldToValue.entrySet()) {\n            String name = field.getKey();\n            String value = field.getValue();\n            if (name.equals(FIELD_RESOURCE_ID)) {\n                resource.setId(value);\n            } else if (name.equals(FIELD_RESOURCE_TYPE)) {\n                resource.setResourceType(AWSResourceType.valueOf(value));\n            } else if (name.equals(FIELD_REGION)) {\n                resource.setRegion(value);\n            } else if (name.equals(FIELD_OWNER_EMAIL)) {\n                resource.setOwnerEmail(value);\n            } else if (name.equals(FIELD_DESCRIPTION)) {\n                resource.setDescription(value);\n            } else if (name.equals(FIELD_STATE)) {\n                resource.setState(CleanupState.valueOf(value));\n            } else if (name.equals(FIELD_TERMINATION_REASON)) {\n                resource.setTerminationReason(value);\n            } else if (name.equals(FIELD_EXPECTED_TERMINATION_TIME)) {\n                resource.setExpectedTerminationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));\n            } else if (name.equals(FIELD_ACTUAL_TERMINATION_TIME)) {\n                resource.setActualTerminationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));\n            } else if (name.equals(FIELD_NOTIFICATION_TIME)) {\n                resource.setNotificationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));\n            } else if (name.equals(FIELD_LAUNCH_TIME)) {\n                resource.setLaunchTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));\n            } else if (name.equals(FIELD_MARK_TIME)) {\n                resource.setMarkTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis()));\n            } else if (name.equals(FIELD_AWS_RESOURCE_STATE)) {\n                resource.setAWSResourceState(value);\n            } else if (name.equals(FIELD_OPT_OUT_OF_JANITOR)) {\n                resource.setOptOutOfJanitor(\"true\".equals(value));\n            } else {\n                // put all other fields into additional fields\n                resource.setAdditionalField(name, value);\n            }\n        }\n        return resource;\n    }\n\n    public String getAWSResourceState() {\n        return awsResourceState;\n    }\n\n    public void setAWSResourceState(String awsState) {\n        this.awsResourceState = awsState;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getId() {\n        return id;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withId(String resourceId) {\n        setId(resourceId);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public ResourceType getResourceType() {\n        return resourceType;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setResourceType(ResourceType resourceType) {\n        this.resourceType = resourceType;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withResourceType(ResourceType type) {\n        setResourceType(type);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getRegion() {\n        return region;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setRegion(String region) {\n        this.region = region;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withRegion(String resourceRegion) {\n        setRegion(resourceRegion);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getOwnerEmail() {\n        return ownerEmail;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setOwnerEmail(String ownerEmail) {\n        this.ownerEmail = ownerEmail;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withOwnerEmail(String resourceOwner) {\n        setOwnerEmail(resourceOwner);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getDescription() {\n        return description;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setDescription(String description) {\n        this.description = description;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withDescription(String resourceDescription) {\n        setDescription(resourceDescription);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Date getLaunchTime() {\n        return getCopyOfDate(launchTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setLaunchTime(Date launchTime) {\n        this.launchTime = getCopyOfDate(launchTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withLaunchTime(Date resourceLaunchTime) {\n        setLaunchTime(resourceLaunchTime);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Date getMarkTime() {\n        return getCopyOfDate(markTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setMarkTime(Date markTime) {\n        this.markTime = getCopyOfDate(markTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withMarkTime(Date resourceMarkTime) {\n        setMarkTime(resourceMarkTime);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Date getExpectedTerminationTime() {\n        return getCopyOfDate(expectedTerminationTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setExpectedTerminationTime(Date expectedTerminationTime) {\n        this.expectedTerminationTime = getCopyOfDate(expectedTerminationTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withExpectedTerminationTime(Date resourceExpectedTerminationTime) {\n        setExpectedTerminationTime(resourceExpectedTerminationTime);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Date getActualTerminationTime() {\n        return getCopyOfDate(actualTerminationTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setActualTerminationTime(Date actualTerminationTime) {\n        this.actualTerminationTime = getCopyOfDate(actualTerminationTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withActualTerminationTime(Date resourceActualTerminationTime) {\n        setActualTerminationTime(resourceActualTerminationTime);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Date getNotificationTime() {\n        return getCopyOfDate(notificationTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setNotificationTime(Date notificationTime) {\n        this.notificationTime = getCopyOfDate(notificationTime);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withNnotificationTime(Date resourceNotificationTime) {\n        setNotificationTime(resourceNotificationTime);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public CleanupState getState() {\n        return state;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setState(CleanupState state) {\n        this.state = state;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withState(CleanupState resourceState) {\n        setState(resourceState);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getTerminationReason() {\n        return terminationReason;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setTerminationReason(String terminationReason) {\n        this.terminationReason = terminationReason;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withTerminationReason(String resourceTerminationReason) {\n        setTerminationReason(resourceTerminationReason);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean isOptOutOfJanitor() {\n        return optOutOfJanitor;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setOptOutOfJanitor(boolean optOutOfJanitor) {\n        this.optOutOfJanitor = optOutOfJanitor;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Resource withOptOutOfJanitor(boolean optOut) {\n        setOptOutOfJanitor(optOut);\n        return this;\n    }\n\n    private static Date getCopyOfDate(Date date) {\n        if (date == null) {\n            return null;\n        }\n        return new Date(date.getTime());\n    }\n\n    private static void putToMapIfNotNull(Map<String, String> map, String key, String value) {\n        Validate.notNull(map);\n        Validate.notNull(key);\n        if (value != null) {\n            map.put(key, value);\n        }\n    }\n\n    private static void putToMapIfNotNull(Map<String, String> map, String key, Enum<?> value) {\n        Validate.notNull(map);\n        Validate.notNull(key);\n        if (value != null) {\n            map.put(key, value.name());\n        }\n    }\n\n    private static void putToMapIfNotNull(Map<String, String> map, String key, NamedType value) {\n        Validate.notNull(map);\n        Validate.notNull(key);\n        if (value != null) {\n            map.put(key, value.name());\n        }\n    }\n\n    private static String printDate(Date date) {\n        if (date == null) {\n            return null;\n        }\n\n        return DATE_FORMATTER.print(date.getTime());\n    }\n\n    @Override\n    public Resource setAdditionalField(String fieldName, String fieldValue) {\n        Validate.notNull(fieldName);\n        Validate.notNull(fieldValue);\n        putToMapIfNotNull(additionalFields, fieldName, fieldValue);\n        return this;\n    }\n\n    @Override\n    public String getAdditionalField(String fieldName) {\n        return additionalFields.get(fieldName);\n    }\n\n    @Override\n    public Collection<String> getAdditionalFieldNames() {\n        return additionalFields.keySet();\n    }\n\n    @Override\n    public Resource cloneResource() {\n        Resource clone = new AWSResource()\n        .withActualTerminationTime(getActualTerminationTime())\n        .withDescription(getDescription())\n        .withExpectedTerminationTime(getExpectedTerminationTime())\n        .withId(getId())\n        .withLaunchTime(getLaunchTime())\n        .withMarkTime(getMarkTime())\n        .withNnotificationTime(getNotificationTime())\n        .withOwnerEmail(getOwnerEmail())\n        .withRegion(getRegion())\n        .withResourceType(getResourceType())\n        .withState(getState())\n        .withTerminationReason(getTerminationReason())\n        .withOptOutOfJanitor(isOptOutOfJanitor());\n        ((AWSResource) clone).setAWSResourceState(awsResourceState);\n\n        ((AWSResource) clone).additionalFields.putAll(additionalFields);\n\n        for (String key : this.getAllTagKeys()) {\n            clone.setTag(key, this.getTag(key));\n        }\n\n        return clone;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void setTag(String key, String value) {\n        tags.put(key, value);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getTag(String key) {\n        return tags.get(key);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Collection<String> getAllTagKeys() {\n        return tags.keySet();\n    }\n\n    @Override\n    public String toString() {\n        return \"AWSResource{\" +\n                \"id='\" + id + '\\'' +\n                \", resourceType=\" + resourceType +\n                \", region='\" + region + '\\'' +\n                \", ownerEmail='\" + ownerEmail + '\\'' +\n                \", description='\" + description + '\\'' +\n                \", terminationReason='\" + terminationReason + '\\'' +\n                \", state=\" + state +\n                \", expectedTerminationTime=\" + expectedTerminationTime +\n                \", actualTerminationTime=\" + actualTerminationTime +\n                \", notificationTime=\" + notificationTime +\n                \", launchTime=\" + launchTime +\n                \", markTime=\" + markTime +\n                \", optOutOfJanitor=\" + optOutOfJanitor +\n                \", awsResourceState='\" + awsResourceState + '\\'' +\n                \", additionalFields=\" + additionalFields +\n                \", tags=\" + tags +\n                '}';\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        // consider two resources to be equivalent if id, resourceType and region match\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        AWSResource that = (AWSResource) o;\n        return Objects.equals(id, that.id) &&\n                Objects.equals(resourceType, that.resourceType) &&\n                Objects.equals(region, that.region);\n    }\n\n    @Override\n    public int hashCode() {\n        // consider two resources to be equivalent if id, resourceType and region match\n        return Objects.hash(id, resourceType, region);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/AWSResourceType.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws;\n\nimport com.netflix.simianarmy.ResourceType;\n\n/**\n * The enum of resource types of AWS.\n */\npublic enum AWSResourceType implements ResourceType {\n    /** AWS instance. */\n    INSTANCE,\n    /** AWS EBS volume. */\n    EBS_VOLUME,\n    /** AWS EBS snapshot. */\n    EBS_SNAPSHOT,\n    /** AWS auto scaling group. */\n    ASG,\n    /** AWS launch configuration. */\n    LAUNCH_CONFIG,\n    /** AWS S3 bucket. */\n    S3_BUCKET,\n    /** AWS security group. */\n    SECURITY_GROUP,\n    /** AWS Amazon Machine Image. **/\n    IMAGE,\n    /** AWS Elastic Load Balancer. **/\n    ELB\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/RDSRecorder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws;\n\nimport com.amazonaws.AmazonClientException;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.MonkeyRecorder;\nimport com.netflix.simianarmy.MonkeyType;\nimport com.netflix.simianarmy.basic.BasicRecorderEvent;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.core.RowMapper;\n\nimport java.io.IOException;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The Class RDSRecorder. Records events to and fetched events from a RDS table (default SIMIAN_ARMY)\n */\n@SuppressWarnings(\"serial\")\npublic class RDSRecorder implements MonkeyRecorder {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(RDSRecorder.class);\n\n    private final String region;\n\n    /** The table. */\n    private final String table;\n    \n    /** the jdbcTemplate  */\n    JdbcTemplate jdbcTemplate = null;\n\n    public static final String FIELD_ID = \"eventId\";\n    public static final String FIELD_EVENT_TIME = \"eventTime\";\n    public static final String FIELD_MONKEY_TYPE = \"monkeyType\";\n    public static final String FIELD_EVENT_TYPE = \"eventType\";\n    public static final String FIELD_REGION = \"region\";\n    public static final String FIELD_DATA_JSON = \"dataJson\";\n        \n    /**\n     * Instantiates a new RDS recorder.\n     *\n     */\n    public RDSRecorder(String dbDriver, String dbUser,\n\t\t\tString dbPass, String dbUrl, String dbTable, String region) {\n        HikariDataSource dataSource = new HikariDataSource();\n        dataSource.setDriverClassName(dbDriver);\n        dataSource.setJdbcUrl(dbUrl);\n        dataSource.setUsername(dbUser);\n        dataSource.setPassword(dbPass);\n        dataSource.setMaximumPoolSize(2);\n    \tthis.jdbcTemplate = new JdbcTemplate(dataSource);\n    \tthis.table = dbTable;\n    \tthis.region = region;\n    }\n\n    /**\n     * Instantiates a new RDS recorder.  This constructor is intended\n     * for unit testing.\n     *\n     */\n    public RDSRecorder(JdbcTemplate jdbcTemplate, String table, String region) {\n    \tthis.jdbcTemplate = jdbcTemplate;\n    \tthis.table = table;\n    \tthis.region = region;\n    }\n    \n    public JdbcTemplate getJdbcTemplate() {\n\t\treturn jdbcTemplate;\n\t}\n\n    /** {@inheritDoc} */\n    @Override\n    public Event newEvent(MonkeyType monkeyType, EventType eventType, String reg, String id) {\n        return new BasicRecorderEvent(monkeyType, eventType, reg, id);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void recordEvent(Event evt) {    \t\n        String evtTime = String.valueOf(evt.eventTime().getTime());\n        String name = String.format(\"%s-%s-%s-%s\", evt.monkeyType().name(), evt.id(), region, evtTime);\n    \tString json;\n\t\ttry {\n\t\t\tjson = new ObjectMapper().writeValueAsString(evt.fields());\n\t\t} catch (JsonProcessingException e) {\n\t\t\tLOGGER.error(\"ERROR generating JSON when saving resource \" + name, e);\n\t\t\treturn;\n\t\t}\n        \n        LOGGER.debug(String.format(\"Saving event %s to RDS table %s\", name, table));    \t        \n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(\"insert into \").append(table);\n\t\tsb.append(\" (\");\n\t\tsb.append(FIELD_ID).append(\",\");\n\t\tsb.append(FIELD_EVENT_TIME).append(\",\");\n\t\tsb.append(FIELD_MONKEY_TYPE).append(\",\");\n\t\tsb.append(FIELD_EVENT_TYPE).append(\",\");\n\t\tsb.append(FIELD_REGION).append(\",\");\n\t\tsb.append(FIELD_DATA_JSON).append(\") values (?,?,?,?,?,?)\");\n\n        LOGGER.debug(String.format(\"Insert statement is '%s'\", sb));\n        int updated = this.jdbcTemplate.update(sb.toString(),\n\t\t\t\t\t\t\t\t\t\t\t   evt.id(),\n\t\t\t\t\t\t\t\t\t\t\t   evt.eventTime().getTime(),\n\t\t\t\t\t\t\t\t\t\t\t   SimpleDBRecorder.enumToValue(evt.monkeyType()),\n\t\t\t\t\t\t\t\t\t\t\t   SimpleDBRecorder.enumToValue(evt.eventType()),\n\t\t\t\t\t\t\t\t\t\t\t   evt.region(),\n\t\t\t\t\t\t\t\t\t\t\t   json);    \t\n        LOGGER.debug(String.format(\"%d rows inserted\", updated));        \n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<Event> findEvents(Map<String, String> query, Date after) {\n        return findEvents(null, null, query, after);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<Event> findEvents(MonkeyType monkeyType, Map<String, String> query, Date after) {\n        return findEvents(monkeyType, null, query, after);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<Event> findEvents(MonkeyType monkeyType, EventType eventType, Map<String, String> query, Date after) {\n        ArrayList<Object> args = new ArrayList<>();\n        StringBuilder sqlquery = new StringBuilder(\n                String.format(\"select * from %s where region = ?\", table));\n        args.add(region);\n        \n        if (monkeyType != null) {\n        \tsqlquery.append(String.format(\" and %s = ?\", FIELD_MONKEY_TYPE));\n        \targs.add(SimpleDBRecorder.enumToValue(monkeyType));\n        }\n\n        if (eventType != null) {\n        \tsqlquery.append(String.format(\" and %s = ?\", FIELD_EVENT_TYPE));\n        \targs.add(SimpleDBRecorder.enumToValue(eventType));\n        }\n        \n        for (Map.Entry<String, String> pair : query.entrySet()) {\n        \tsqlquery.append(String.format(\" and %s like ?\", FIELD_DATA_JSON));\n            args.add((String.format(\"%s: \\\"%s\\\"\", pair.getKey(), pair.getValue())));\n        }\n        sqlquery.append(String.format(\" and %s > ? order by %s desc\", FIELD_EVENT_TIME, FIELD_EVENT_TIME));\n        args.add(new Long(after.getTime()));\n        \n        LOGGER.debug(String.format(\"Query is '%s'\", sqlquery));\n        List<Event> events = jdbcTemplate.query(sqlquery.toString(), args.toArray(), new RowMapper<Event>() {\n            public Event mapRow(ResultSet rs, int rowNum) throws SQLException {\n            \treturn mapEvent(rs);                \n            }             \n        });                \n        return events;\n    }\n    \n    private Event mapEvent(ResultSet rs) throws SQLException {\n    \tString json = rs.getString(\"dataJson\");\n    \tObjectMapper mapper = new ObjectMapper();\n    \tEvent event = null;\n    \ttry {\n    \t\tString id = rs.getString(FIELD_ID);\n    \t\tMonkeyType monkeyType = SimpleDBRecorder.valueToEnum(MonkeyType.class, rs.getString(FIELD_MONKEY_TYPE));\n    \t\tEventType eventType = SimpleDBRecorder.valueToEnum(EventType.class, rs.getString(FIELD_EVENT_TYPE));\n    \t\tString region = rs.getString(FIELD_REGION);\n    \t\tlong time = rs.getLong(FIELD_EVENT_TIME);    \t\t\n    \t    event = new BasicRecorderEvent(monkeyType, eventType, region, id, time);\n\n            TypeReference<Map<String,String>> typeRef = new TypeReference<Map<String,String>>() {};\n    \t\tMap<String, String> map = mapper.readValue(json, typeRef);\n    \t    for(String key : map.keySet()) {\n    \t    \tevent.addField(key, map.get(key));\n    \t    }\n\n    \t}catch(IOException ie) {\n    \t\tLOGGER.error(\"Error parsing resource from json\", ie);\n    \t}    \t\n        return event;\n    }             \n                   \n\n    /**\n     * Creates the RDS table, if it does not already exist.\n     */\n    public void init() {\n        try {\n            if (this.region == null || this.region.equals(\"region-null\")) {\n                // This is a mock with an invalid region; avoid a slow timeout\n                LOGGER.debug(\"Region=null; skipping RDS table creation\");\n                return;\n            }\n            \n            LOGGER.info(\"Creating RDS table: {}\", table);\n            String sql = String.format(\"create table if not exists %s (\"\n                                     + \" %s varchar(255),\"\n                                     + \" %s BIGINT,\"\n                                     + \" %s varchar(255),\"\n                                     + \" %s varchar(255),\"\n                                     + \" %s varchar(255),\"\n                                     + \" %s varchar(4096) )\", \n                                     table, \n                                     FIELD_ID, \n                                     FIELD_EVENT_TIME, \n                                     FIELD_MONKEY_TYPE, \n                                     FIELD_EVENT_TYPE, \n                                     FIELD_REGION, \n                                     FIELD_DATA_JSON);\n            LOGGER.debug(\"Create SQL is: '{}'\", sql);\n            jdbcTemplate.execute(sql);\n            \n        } catch (AmazonClientException e) {\n            LOGGER.warn(\"Error while trying to auto-create RDS table\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/STSAssumeRoleSessionCredentialsProvider.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws;\n\nimport java.util.Date;\n\nimport com.amazonaws.ClientConfiguration;\nimport com.amazonaws.auth.AWSCredentials;\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.auth.AWSSessionCredentials;\nimport com.amazonaws.auth.BasicSessionCredentials;\nimport com.amazonaws.services.securitytoken.AWSSecurityTokenService;\nimport com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient;\nimport com.amazonaws.services.securitytoken.model.AssumeRoleRequest;\nimport com.amazonaws.services.securitytoken.model.AssumeRoleResult;\nimport com.amazonaws.services.securitytoken.model.Credentials;\n\n/**\n * AWSCredentialsProvider implementation that uses the AWS Security Token\n * Service to assume a Role and create temporary, short-lived sessions to use\n * for authentication.\n */\npublic class STSAssumeRoleSessionCredentialsProvider implements AWSCredentialsProvider {\n\n    /** Default duration for started sessions. */\n    public static final int DEFAULT_DURATION_SECONDS = 900;\n\n    /** Time before expiry within which credentials will be renewed. */\n    private static final int EXPIRY_TIME_MILLIS = 60 * 1000;\n\n    /** The client for starting STS sessions. */\n    private final AWSSecurityTokenService securityTokenService;\n\n    /** The current session credentials. */\n    private AWSSessionCredentials sessionCredentials;\n\n    /** The expiration time for the current session credentials. */\n    private Date sessionCredentialsExpiration;\n\n    /** The arn of the role to be assumed. */\n    private String roleArn;\n\n    /**\n     * Constructs a new STSAssumeRoleSessionCredentialsProvider, which makes a\n     * request to the AWS Security Token Service (STS), uses the provided\n     * {@link #roleArn} to assume a role and then request short lived session\n     * credentials, which will then be returned by this class's\n     * {@link #getCredentials()} method.\n     * @param roleArn\n     *            The AWS ARN of the Role to be assumed.\n     */\n    public STSAssumeRoleSessionCredentialsProvider(String roleArn) {\n        this.roleArn = roleArn;\n        securityTokenService = new AWSSecurityTokenServiceClient();\n    }\n\n    /**\n     * Constructs a new STSAssumeRoleSessionCredentialsProvider, which makes a\n     * request to the AWS Security Token Service (STS), uses the provided\n     * {@link #roleArn} to assume a role and then request short lived session\n     * credentials, which will then be returned by this class's\n     * {@link #getCredentials()} method.\n     * @param roleArn\n     *            The AWS ARN of the Role to be assumed.\n     * @param clientConfiguration\n     *            The AWS ClientConfiguration to use when making AWS API requests.\n     */\n    public STSAssumeRoleSessionCredentialsProvider(String roleArn, ClientConfiguration clientConfiguration) {\n        this.roleArn = roleArn;\n        securityTokenService = new AWSSecurityTokenServiceClient(clientConfiguration);\n    }\n\n    /**\n     * Constructs a new STSAssumeRoleSessionCredentialsProvider, which will use\n     * the specified long lived AWS credentials to make a request to the AWS\n     * Security Token Service (STS), uses the provided {@link #roleArn} to\n     * assume a role and then request short lived session credentials, which\n     * will then be returned by this class's {@link #getCredentials()} method.\n     * @param longLivedCredentials\n     *            The main AWS credentials for a user's account.\n     * @param roleArn\n     *            The AWS ARN of the Role to be assumed.\n     */\n    public STSAssumeRoleSessionCredentialsProvider(AWSCredentials longLivedCredentials, String roleArn) {\n        this(longLivedCredentials, roleArn, new ClientConfiguration());\n    }\n\n    /**\n     * Constructs a new STSAssumeRoleSessionCredentialsProvider, which will use\n     * the specified long lived AWS credentials to make a request to the AWS\n     * Security Token Service (STS), uses the provided {@link #roleArn} to\n     * assume a role and then request short lived session credentials, which\n     * will then be returned by this class's {@link #getCredentials()} method.\n     * @param longLivedCredentials\n     *            The main AWS credentials for a user's account.\n     * @param roleArn\n     *            The AWS ARN of the Role to be assumed.\n     * @param clientConfiguration\n     *            Client configuration connection parameters.\n     */\n    public STSAssumeRoleSessionCredentialsProvider(AWSCredentials longLivedCredentials, String roleArn,\n            ClientConfiguration clientConfiguration) {\n        this.roleArn = roleArn;\n        securityTokenService = new AWSSecurityTokenServiceClient(longLivedCredentials, clientConfiguration);\n    }\n\n    /**\n     * Constructs a new STSAssumeRoleSessionCredentialsProvider, which will use\n     * the specified credentials provider (which vends long lived AWS\n     * credentials) to make a request to the AWS Security Token Service (STS),\n     * usess the provided {@link #roleArn} to assume a role and then request\n     * short lived session credentials, which will then be returned by this\n     * class's {@link #getCredentials()} method.\n     * @param longLivedCredentialsProvider\n     *            Credentials provider for the main AWS credentials for a user's\n     *            account.\n     * @param roleArn\n     *            The AWS ARN of the Role to be assumed.\n     */\n    public STSAssumeRoleSessionCredentialsProvider(AWSCredentialsProvider longLivedCredentialsProvider,\n            String roleArn) {\n        this.roleArn = roleArn;\n        securityTokenService = new AWSSecurityTokenServiceClient(longLivedCredentialsProvider);\n    }\n\n    /**\n     * Constructs a new STSAssumeRoleSessionCredentialsProvider, which will use\n     * the specified credentials provider (which vends long lived AWS\n     * credentials) to make a request to the AWS Security Token Service (STS),\n     * uses the provided {@link #roleArn} to assume a role and then request\n     * short lived session credentials, which will then be returned by this\n     * class's {@link #getCredentials()} method.\n     * @param longLivedCredentialsProvider\n     *            Credentials provider for the main AWS credentials for a user's\n     *            account.\n     * @param roleArn\n     *            The AWS ARN of the Role to be assumed.\n     * @param clientConfiguration\n     *            Client configuration connection parameters.\n     */\n    public STSAssumeRoleSessionCredentialsProvider(AWSCredentialsProvider longLivedCredentialsProvider, String roleArn,\n            ClientConfiguration clientConfiguration) {\n        this.roleArn = roleArn;\n        securityTokenService = new AWSSecurityTokenServiceClient(longLivedCredentialsProvider, clientConfiguration);\n    }\n\n    @Override\n    public AWSCredentials getCredentials() {\n        if (needsNewSession()) {\n            startSession();\n        }\n        return sessionCredentials;\n    }\n\n    @Override\n    public void refresh() {\n        startSession();\n    }\n\n    /**\n     * Starts a new session by sending a request to the AWS Security Token\n     * Service (STS) to assume a Role using the long lived AWS credentials. This\n     * class then vends the short lived session credentials for the assumed Role\n     * sent back from STS.\n     */\n    private void startSession() {\n        AssumeRoleResult assumeRoleResult = securityTokenService.assumeRole(new AssumeRoleRequest()\n                .withRoleArn(roleArn).withDurationSeconds(DEFAULT_DURATION_SECONDS).withRoleSessionName(\"SimianArmy\"));\n        Credentials stsCredentials = assumeRoleResult.getCredentials();\n        sessionCredentials = new BasicSessionCredentials(stsCredentials.getAccessKeyId(),\n                stsCredentials.getSecretAccessKey(), stsCredentials.getSessionToken());\n        sessionCredentialsExpiration = stsCredentials.getExpiration();\n    }\n\n    /**\n     * Returns true if a new STS session needs to be started. A new STS session\n     * is needed when no session has been started yet, or if the last session is\n     * within {@link #EXPIRY_TIME_MILLIS} seconds of expiring.\n     * @return True if a new STS session needs to be started.\n     */\n    private boolean needsNewSession() {\n        if (sessionCredentials == null) {\n            return true;\n        }\n        long timeRemaining = sessionCredentialsExpiration.getTime() - System.currentTimeMillis();\n        return timeRemaining < EXPIRY_TIME_MILLIS;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/SimpleDBRecorder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws;\n\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.AmazonClientException;\nimport com.amazonaws.services.simpledb.AmazonSimpleDB;\nimport com.amazonaws.services.simpledb.model.Attribute;\nimport com.amazonaws.services.simpledb.model.CreateDomainRequest;\nimport com.amazonaws.services.simpledb.model.Item;\nimport com.amazonaws.services.simpledb.model.ListDomainsResult;\nimport com.amazonaws.services.simpledb.model.PutAttributesRequest;\nimport com.amazonaws.services.simpledb.model.ReplaceableAttribute;\nimport com.amazonaws.services.simpledb.model.SelectRequest;\nimport com.amazonaws.services.simpledb.model.SelectResult;\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.MonkeyRecorder;\nimport com.netflix.simianarmy.MonkeyType;\nimport com.netflix.simianarmy.NamedType;\nimport com.netflix.simianarmy.basic.BasicRecorderEvent;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\n/**\n * The Class SimpleDBRecorder. Records events to and fetched events from a Amazon SimpleDB table (default SIMIAN_ARMY)\n */\n@SuppressWarnings(\"serial\")\npublic class SimpleDBRecorder implements MonkeyRecorder {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDBRecorder.class);\n\n    private final AmazonSimpleDB simpleDBClient;\n\n    private final String region;\n\n    /** The domain. */\n    private final String domain;\n\n    /**\n     * The Enum Keys.\n     */\n    private enum Keys {\n\n        /** The event id. */\n        id,\n        /** The event time. */\n        eventTime,\n        /** The region. */\n        region,\n        /** The record type. */\n        recordType,\n        /** The monkey type. */\n        monkeyType,\n        /** The event type. */\n        eventType;\n\n        /** The Constant KEYSET. */\n        public static final Set<String> KEYSET = Collections.unmodifiableSet(new HashSet<String>() {\n            {\n                for (Keys k : Keys.values()) {\n                    add(k.toString());\n                }\n            }\n        });\n    };\n\n    /**\n     * Instantiates a new simple db recorder.\n     *\n     * @param awsClient\n     *            the AWS client\n     * @param domain\n     *            the domain\n     */\n    public SimpleDBRecorder(AWSClient awsClient, String domain) {\n        Validate.notNull(awsClient);\n        Validate.notNull(domain);\n        this.simpleDBClient = awsClient.sdbClient();\n        this.region = awsClient.region();\n        this.domain = domain;\n    }\n\n    /**\n     * simple client. abstracted to aid testing\n     *\n     * @return the amazon simple db\n     */\n    protected AmazonSimpleDB sdbClient() {\n        return simpleDBClient;\n    }\n\n    /**\n     * Enum to value. Converts an enum to \"name|type\" string\n     *\n     * @param e\n     *            the e\n     * @return the string\n     */\n    public static String enumToValue(NamedType e) {\n        return String.format(\"%s|%s\", e.name(), e.getClass().getName());\n    }\n\n    /**\n     * Value to enum. Converts a \"name|type\" string back to an enum.\n     *\n     * @param value\n     *            the value\n     * @return the enum\n     */\n    public static <T extends NamedType> T valueToEnum(\n            Class<T> type, String value) {\n        // parts = [enum value, enum class type]\n        String[] parts = value.split(\"\\\\|\", 2);\n        if (parts.length < 2) {\n            throw new RuntimeException(\"value \" + value + \" does not appear to be an internal enum format\");\n        }\n\n        Class<?> enumClass;\n        try {\n            enumClass = Class.forName(parts[1]);\n        } catch (ClassNotFoundException e) {\n            throw new RuntimeException(\"class for enum value \" + value + \" not found\");\n        }\n        if (!enumClass.isEnum()) {\n            throw new RuntimeException(\"value \" + value + \" does not appear to be of an enum type\");\n        }\n        if (!type.isAssignableFrom(enumClass)) {\n            throw new RuntimeException(\"value \" + value + \" cannot be assigned to a variable of this type: \"\n                    + type.getCanonicalName());\n        }\n        @SuppressWarnings(\"rawtypes\")\n        Class<? extends Enum> enumType = enumClass.asSubclass(Enum.class);\n        @SuppressWarnings(\"unchecked\")\n        T enumValue = (T) Enum.valueOf(enumType, parts[0]);\n        return enumValue;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Event newEvent(MonkeyType monkeyType, EventType eventType, String reg, String id) {\n        return new BasicRecorderEvent(monkeyType, eventType, reg, id);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void recordEvent(Event evt) {\n        String evtTime = String.valueOf(evt.eventTime().getTime());\n        List<ReplaceableAttribute> attrs = new LinkedList<ReplaceableAttribute>();\n        attrs.add(new ReplaceableAttribute(Keys.id.name(), evt.id(), true));\n        attrs.add(new ReplaceableAttribute(Keys.eventTime.name(), evtTime, true));\n        attrs.add(new ReplaceableAttribute(Keys.region.name(), evt.region(), true));\n        attrs.add(new ReplaceableAttribute(Keys.recordType.name(), \"MonkeyEvent\", true));\n        attrs.add(new ReplaceableAttribute(Keys.monkeyType.name(), enumToValue(evt.monkeyType()), true));\n        attrs.add(new ReplaceableAttribute(Keys.eventType.name(), enumToValue(evt.eventType()), true));\n        for (Map.Entry<String, String> pair : evt.fields().entrySet()) {\n            if (pair.getValue() == null || pair.getValue().equals(\"\") || Keys.KEYSET.contains(pair.getKey())) {\n                continue;\n            }\n            attrs.add(new ReplaceableAttribute(pair.getKey(), pair.getValue(), true));\n        }\n        // Let pk contain the timestamp so that the same resource can have multiple events.\n        String pk = String.format(\"%s-%s-%s-%s\", evt.monkeyType().name(), evt.id(), region, evtTime);\n        PutAttributesRequest putReq = new PutAttributesRequest(domain, pk, attrs);\n        sdbClient().putAttributes(putReq);\n\n    }\n\n    /**\n     * Find events.\n     *\n     * @param queryMap\n     *            the query map\n     * @param after\n     *            the start time to query for all events after\n     * @return the list\n     */\n    protected List<Event> findEvents(Map<String, String> queryMap, long after) {\n        StringBuilder query = new StringBuilder(\n                String.format(\"select * from `%s` where region = '%s'\", domain, region));\n        for (Map.Entry<String, String> pair : queryMap.entrySet()) {\n            query.append(String.format(\" and %s = '%s'\", pair.getKey(), pair.getValue()));\n        }\n        query.append(String.format(\" and eventTime > '%d'\", after));\n        // always return with most recent record first\n        query.append(\" order by eventTime desc\");\n\n        List<Event> list = new LinkedList<Event>();\n        SelectRequest request = new SelectRequest(query.toString());\n        request.setConsistentRead(Boolean.TRUE);\n\n        SelectResult result = new SelectResult();\n        do {\n            result = sdbClient().select(request.withNextToken(result.getNextToken()));\n            for (Item item : result.getItems()) {\n                Map<String, String> fields = new HashMap<String, String>();\n                Map<String, String> res = new HashMap<String, String>();\n                for (Attribute attr : item.getAttributes()) {\n                    if (Keys.KEYSET.contains(attr.getName())) {\n                        res.put(attr.getName(), attr.getValue());\n                    } else {\n                        fields.put(attr.getName(), attr.getValue());\n                    }\n                }\n                String eid = res.get(Keys.id.name());\n                String ereg = res.get(Keys.region.name());\n                MonkeyType monkeyType = valueToEnum(MonkeyType.class, res.get(Keys.monkeyType.name()));\n                EventType eventType = valueToEnum(EventType.class, res.get(Keys.eventType.name()));\n                long eventTime = Long.parseLong(res.get(Keys.eventTime.name()));\n                list.add(new BasicRecorderEvent(monkeyType, eventType, ereg, eid, eventTime).addFields(fields));\n            }\n        } while (result.getNextToken() != null);\n        return list;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<Event> findEvents(Map<String, String> query, Date after) {\n        return findEvents(query, after.getTime());\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<Event> findEvents(MonkeyType monkeyType, Map<String, String> query, Date after) {\n        Map<String, String> copy = new LinkedHashMap<String, String>(query);\n        copy.put(Keys.monkeyType.name(), enumToValue(monkeyType));\n        return findEvents(copy, after);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<Event> findEvents(MonkeyType monkeyType, EventType eventType, Map<String, String> query, Date after) {\n        Map<String, String> copy = new LinkedHashMap<String, String>(query);\n        copy.put(Keys.monkeyType.name(), enumToValue(monkeyType));\n        copy.put(Keys.eventType.name(), enumToValue(eventType));\n        return findEvents(copy, after);\n    }\n\n    /**\n     * Creates the SimpleDB domain, if it does not already exist.\n     */\n    public void init() {\n        try {\n            if (this.region == null || this.region.equals(\"region-null\")) {\n                // This is a mock with an invalid region; avoid a slow timeout\n                LOGGER.debug(\"Region=null; skipping SimpleDB domain creation\");\n                return;\n            }\n            ListDomainsResult listDomains = sdbClient().listDomains();\n            for (String d : listDomains.getDomainNames()) {\n                if (d.equals(domain)) {\n                    LOGGER.debug(\"SimpleDB domain found: {}\", domain);\n                    return;\n                }\n            }\n            LOGGER.info(\"Creating SimpleDB domain: {}\", domain);\n            CreateDomainRequest createDomainRequest = new CreateDomainRequest(\n                    domain);\n            sdbClient().createDomain(createDomainRequest);\n        } catch (AmazonClientException e) {\n            LOGGER.warn(\"Error while trying to auto-create SimpleDB domain\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/RDSConformityClusterTracker.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity;\n\nimport com.amazonaws.AmazonClientException;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityClusterTracker;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.core.RowMapper;\n\nimport java.io.IOException;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.sql.Types;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The RDSConformityClusterTracker implementation in RDS (relational database).\n */\npublic class RDSConformityClusterTracker implements ConformityClusterTracker {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(RDSConformityClusterTracker.class);\n\n    /** The table. */\n    private final String table;\n    \n    /** the jdbcTemplate  */\n    JdbcTemplate jdbcTemplate = null;\n    \n    /**\n     * Instantiates a new RDS db resource tracker.\n     *\n     */\n    public RDSConformityClusterTracker(String dbDriver, String dbUser,\n\t\t\tString dbPass, String dbUrl, String dbTable) {\n\t\tHikariDataSource dataSource = new HikariDataSource();\n        dataSource.setDriverClassName(dbDriver);\n\t\tdataSource.setJdbcUrl(dbUrl);\n\t\tdataSource.setUsername(dbUser);\n\t\tdataSource.setPassword(dbPass);\n\t\tdataSource.setMaximumPoolSize(2);\n    \tthis.jdbcTemplate = new JdbcTemplate(dataSource);\n    \tthis.table = dbTable;\n\t}\n\n    /**\n     * Instantiates a new RDS conformity cluster tracker.  This constructor is intended\n     * for unit testing.\n     *\n     */\n    public RDSConformityClusterTracker(JdbcTemplate jdbcTemplate, String table) {\n    \tthis.jdbcTemplate = jdbcTemplate;\n    \tthis.table = table;\n    }\n    \n    public JdbcTemplate getJdbcTemplate() {\n\t\treturn jdbcTemplate;\n\t}\n\n    public Object value(String value) {\n    \treturn value == null ? Types.NULL : value;\n    }\n\n    public Object value(Date value) {\n    \treturn value == null ? Types.NULL : value.getTime();\n    }    \n\n    public Object value(boolean value) {\n    \treturn Boolean.toString(value);\n    }\n\n\tpublic Object emailValue(String email) {\n\t\tif (StringUtils.isBlank(email)) return Types.NULL;\n\t\tif (email.equals(\"0\")) return Types.NULL;\n\t\treturn email;\n\t}\n\n    /** {@inheritDoc} */\n    @Override\n    public void addOrUpdate(Cluster cluster) {\n    \tCluster orig = getCluster(cluster.getName(), cluster.getRegion());    \t\n        LOGGER.debug(String.format(\"Saving cluster %s to RDB table %s in region %s\", cluster.getName(), cluster.getRegion(), table));\n\t\tMap<String, String> map = cluster.getFieldToValueMap();\n\n    \tString conformityJson;\n\t\ttry {\n\t\t\tconformityJson = new ObjectMapper().writeValueAsString(conformitiesAsMap(cluster));\n\t\t} catch (JsonProcessingException e) {\n\t\t\tLOGGER.error(\"ERROR generating conformities JSON when saving cluster \" + cluster.getName() + \", \" + cluster.getRegion(), e);\n\t\t\treturn;\n\t\t}\n\t\t\n    \tif (orig == null) {\n    \t\tStringBuilder sb = new StringBuilder();\n    \t\tsb.append(\"insert into \").append(table);\n    \t\tsb.append(\" (\");\n    \t\tsb.append(Cluster.CLUSTER).append(\",\");\n    \t\tsb.append(Cluster.REGION).append(\",\");\n    \t\tsb.append(Cluster.OWNER_EMAIL).append(\",\");\n    \t\tsb.append(Cluster.IS_CONFORMING).append(\",\");\n    \t\tsb.append(Cluster.IS_OPTEDOUT).append(\",\");\n    \t\tsb.append(Cluster.UPDATE_TIMESTAMP).append(\",\");\n    \t\tsb.append(Cluster.EXCLUDED_RULES).append(\",\");\n    \t\tsb.append(\"conformities\").append(\",\");\n    \t\tsb.append(Cluster.CONFORMITY_RULES);\n    \t\tsb.append(\") values (?,?,?,?,?,?,?,?,?)\");\n\n            LOGGER.debug(String.format(\"Insert statement is '%s'\", sb));\n    \t\tthis.jdbcTemplate.update(sb.toString(),\n    \t\t\t\t\t\t\t\t value(map.get(Cluster.CLUSTER)),\n    \t\t\t\t\t\t\t\t value(map.get(Cluster.REGION)),\n\t\t\t\t\t\t\t\t\t emailValue(map.get(Cluster.OWNER_EMAIL)),\n    \t\t\t\t\t\t\t\t value(map.get(Cluster.IS_CONFORMING)),\n    \t\t\t\t\t\t\t\t value(map.get(Cluster.IS_OPTEDOUT)),\n    \t\t\t\t\t\t\t\t value(cluster.getUpdateTime()),\n    \t\t\t\t\t\t\t\t value(map.get(Cluster.EXCLUDED_RULES)),\n    \t\t\t\t\t\t\t\t value(conformityJson),\n    \t\t\t\t\t\t\t\t value(map.get(Cluster.CONFORMITY_RULES)));\n    \t} else {\n    \t\tStringBuilder sb = new StringBuilder();\n    \t\tsb.append(\"update \").append(table).append(\" set \");\n    \t\tsb.append(Cluster.OWNER_EMAIL).append(\"=?,\");\n    \t\tsb.append(Cluster.IS_CONFORMING).append(\"=?,\");\n    \t\tsb.append(Cluster.IS_OPTEDOUT).append(\"=?,\");\n    \t\tsb.append(Cluster.UPDATE_TIMESTAMP).append(\"=?,\");\n    \t\tsb.append(Cluster.EXCLUDED_RULES).append(\"=?,\");\n    \t\tsb.append(\"conformities\").append(\"=?,\");\n    \t\tsb.append(Cluster.CONFORMITY_RULES).append(\"=? where \");\n    \t\tsb.append(Cluster.CLUSTER).append(\"=? and \");\n    \t\tsb.append(Cluster.REGION).append(\"=?\");\n\n            LOGGER.debug(String.format(\"Update statement is '%s'\", sb));\n    \t\tthis.jdbcTemplate.update(sb.toString(),\n    \t\t\t\t\t\t\t\temailValue(map.get(Cluster.OWNER_EMAIL)),\n    \t\t\t\t\t\t\t\tvalue(map.get(Cluster.IS_CONFORMING)),\n    \t\t\t\t\t\t\t\tvalue(map.get(Cluster.IS_OPTEDOUT)),\n    \t\t\t\t\t\t\t\tvalue(cluster.getUpdateTime()),\n    \t\t\t\t\t\t\t\tvalue(map.get(Cluster.EXCLUDED_RULES)),\n    \t\t\t\t\t\t\t\tvalue(conformityJson),\n    \t\t\t\t\t\t\t\tvalue(map.get(Cluster.CONFORMITY_RULES)),\n    \t\t\t\t\t\t\t\tvalue(cluster.getName()),\n\t\t\t\t\t\t\t\t\tvalue(cluster.getRegion()));    \t\n    \t}\n    \tLOGGER.debug(\"Successfully saved.\");\n    }\n\n    private HashMap<String,String> conformitiesAsMap(Cluster cluster) {\n    \tHashMap<String,String> map = new HashMap<>();\n    \t\n    \tfor(Conformity conformity : cluster.getConformties()) {\n            map.put(conformity.getRuleId(), StringUtils.join(conformity.getFailedComponents(), \",\"));\n    \t}\n    \t\n\t\treturn map;\n\t}\n    \n\t/**\n     * Gets the clusters for a list of regions. If the regions parameter is empty, returns the clusters\n     * for all regions.\n     */\n    @Override\n    public List<Cluster> getAllClusters(String... regions) {\n        return getClusters(null, regions);\n    }\n\n    @Override\n    public List<Cluster> getNonconformingClusters(String... regions) {\n        return getClusters(false, regions);\n    }\n\n    @Override\n    public Cluster getCluster(String clusterName, String region) {\n        Validate.notEmpty(clusterName);\n        Validate.notEmpty(region);\n        StringBuilder query = new StringBuilder();\n        query.append(String.format(\"select * from %s where cluster = ? and region = ?\", table));\n        LOGGER.info(String.format(\"Query is '%s'\", query));\n\n        List<Cluster> clusters = jdbcTemplate.query(query.toString(), new String[] {clusterName, region}, new RowMapper<Cluster>() {\n            public Cluster mapRow(ResultSet rs, int rowNum) throws SQLException {\n            \treturn mapResource(rs);                \n            }             \n        });                \n        Validate.isTrue(clusters.size() <= 1);\n        if (clusters.size() == 0) {\n            LOGGER.info(String.format(\"Not found cluster with name %s in region %s\", clusterName, region));\n            return null;\n        } else {\n            Cluster cluster = clusters.get(0);\n            return cluster;\n        }\n    }\n    \n    private Cluster mapResource(ResultSet rs) throws SQLException {\n    \tMap<String, String> map = conformityMapFromJson(rs.getString(\"conformities\"));\n\t\tmap.put(Cluster.CLUSTER, rs.getString(Cluster.CLUSTER));\n\t\tmap.put(Cluster.REGION, rs.getString(Cluster.REGION));\n\t\tmap.put(Cluster.IS_CONFORMING, rs.getString(Cluster.IS_CONFORMING));\n\t\tmap.put(Cluster.IS_OPTEDOUT, rs.getString(Cluster.IS_OPTEDOUT));\n\n\t\tString email = rs.getString(Cluster.OWNER_EMAIL);\n\t\tif (StringUtils.isBlank(email) || email.equals(\"0\")) {\n\t\t\temail = null;\n\t\t}\n\t\tmap.put(Cluster.OWNER_EMAIL, email);\n\n\t\tString updatedTimestamp = millisToFormattedDate(rs.getString(Cluster.UPDATE_TIMESTAMP));\n\t\tif (updatedTimestamp != null) {\n\t\t\tmap.put(Cluster.UPDATE_TIMESTAMP, updatedTimestamp);\n\t\t}\n\t\t\n\t\tmap.put(Cluster.EXCLUDED_RULES, rs.getString(Cluster.EXCLUDED_RULES));\n\t\tmap.put(Cluster.CONFORMITY_RULES, rs.getString(Cluster.CONFORMITY_RULES));\n\t\treturn Cluster.parseFieldToValueMap(map);\n    }                 \n    \n    private String millisToFormattedDate(String millisStr) {\n    \tString datetime = null;\n    \ttry {\n    \t\tlong millis = Long.parseLong(millisStr);\n    \t\tdatetime = AWSResource.DATE_FORMATTER.print(millis);\n    \t} catch(NumberFormatException nfe) {\n\t\t\tLOGGER.error(String.format(\"Error parsing datetime %s when reading from RDS\", millisStr));\n    \t}\n    \treturn datetime;\n    }\n    \n    private HashMap<String,String> conformityMapFromJson(String json) throws SQLException {\n    \tHashMap<String,String> map = new HashMap<>();\n    \t\n    \tif (json != null) {\t    \t\n\t    \tTypeReference<HashMap<String,String>> typeRef = new TypeReference<HashMap<String,String>>() {};\n\t    \t\n\t    \ttry {\n\t        \tObjectMapper mapper = new ObjectMapper();\n\t    \t\tmap = mapper.readValue(json, typeRef);\n\t    \t}catch(IOException ie) {\n\t    \t\tString msg = \"Error parsing conformities from result set\";\n\t    \t\tLOGGER.error(msg, ie);\n\t    \t\tthrow new SQLException(msg);    \t\t\n\t    \t}    \t\n    \t}\n\t\treturn map;\n\t}\n\n\t@Override\n    public void deleteClusters(Cluster... clusters) {\n        Validate.notNull(clusters);\n        LOGGER.info(String.format(\"Deleting %d clusters\", clusters.length));\n        for (Cluster cluster : clusters) {\n            LOGGER.info(String.format(\"Deleting cluster %s\", cluster.getName()));\n            String stmt = String.format(\"delete from %s where %s=? and %s=?\", table, Cluster.CLUSTER, Cluster.REGION);\n            jdbcTemplate.update(stmt, cluster.getName(), cluster.getRegion());\n            LOGGER.info(String.format(\"Successfully deleted cluster %s\", cluster.getName()));\n        }\n    }\n\n    private List<Cluster> getClusters(Boolean conforming, String... regions) {\n        Validate.notNull(regions);\n        StringBuilder query = new StringBuilder();\n        query.append(String.format(\"select * from %s where cluster is not null and \", table));\n        boolean needsAnd = false;\n        if (regions.length != 0) {\n            query.append(String.format(\"region in ('%s') \", StringUtils.join(regions, \"','\")));\n            needsAnd = true;\n        }\n        if (conforming != null) {\n            if (needsAnd) {\n                query.append(\" and \");\n            }\n            query.append(String.format(\"isConforming = '%s'\", conforming));\n        }\n\n        LOGGER.info(String.format(\"Query to retrieve clusters for regions %s is '%s'\",\n                StringUtils.join(regions, \"','\"), query.toString()));\n\n        List<Cluster> clusters = jdbcTemplate.query(query.toString(), new RowMapper<Cluster>() {\n            public Cluster mapRow(ResultSet rs, int rowNum) throws SQLException {\n            \treturn mapResource(rs);                \n            }             \n        });                \n        \n        LOGGER.info(String.format(\"Retrieved %d clusters from RDS DB in table %s and regions %s\",\n                clusters.size(), table, StringUtils.join(regions, \"','\")));\n        return clusters;\n    }\n    \n    /**\n     * Creates the RDS table, if it does not already exist.\n     */\n    public void init() {\n        try {\n            LOGGER.info(\"Creating RDS table: {}\", table);\n            String sql = String.format(\"create table if not exists %s (\"\n                                     + \" %s varchar(255),\"\n                                     + \" %s varchar(25),\"\n                                     + \" %s varchar(255),\"\n                                     + \" %s varchar(10),\"\n                                     + \" %s varchar(10),\"\n                                     + \" %s BIGINT,\" \n                                     + \" %s varchar(4096),\"\n                                     + \" %s varchar(4096),\"\n                                     + \" %s varchar(4096) )\",\n                                     table,\n                                     Cluster.CLUSTER,\n                                     Cluster.REGION,\n                                     Cluster.OWNER_EMAIL,\n                                     Cluster.IS_CONFORMING,\n                                     Cluster.IS_OPTEDOUT,\n                                     Cluster.UPDATE_TIMESTAMP,\n                                     Cluster.EXCLUDED_RULES,\n                                     \"conformities\",\n                                     Cluster.CONFORMITY_RULES);\n           LOGGER.debug(\"Create SQL is: '{}'\", sql);\n           jdbcTemplate.execute(sql);\n            \n        } catch (AmazonClientException e) {\n            LOGGER.warn(\"Error while trying to auto-create RDS table\", e);\n        }\n    }    \n}"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/SimpleDBConformityClusterTracker.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity;\n\nimport com.amazonaws.services.simpledb.AmazonSimpleDB;\nimport com.amazonaws.services.simpledb.model.Attribute;\nimport com.amazonaws.services.simpledb.model.DeleteAttributesRequest;\nimport com.amazonaws.services.simpledb.model.Item;\nimport com.amazonaws.services.simpledb.model.PutAttributesRequest;\nimport com.amazonaws.services.simpledb.model.ReplaceableAttribute;\nimport com.amazonaws.services.simpledb.model.SelectRequest;\nimport com.amazonaws.services.simpledb.model.SelectResult;\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.ConformityClusterTracker;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The ConformityResourceTracker implementation in SimpleDB.\n */\npublic class SimpleDBConformityClusterTracker implements ConformityClusterTracker {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDBConformityClusterTracker.class);\n\n    /** The domain. */\n    private final String domain;\n\n    /** The SimpleDB client. */\n    private final AmazonSimpleDB simpleDBClient;\n\n    private static final int  MAX_ATTR_SIZE = 1024;\n\n    /**\n     * Instantiates a new simple db cluster tracker for conformity monkey.\n     *\n     * @param awsClient\n     *            the AWS Client\n     * @param domain\n     *            the domain\n     */\n    public SimpleDBConformityClusterTracker(AWSClient awsClient, String domain) {\n        Validate.notNull(awsClient);\n        Validate.notNull(domain);\n        this.domain = domain;\n        this.simpleDBClient = awsClient.sdbClient();\n    }\n\n    /**\n     * Gets the SimpleDB client.\n     * @return the SimpleDB client\n     */\n    protected AmazonSimpleDB getSimpleDBClient() {\n        return simpleDBClient;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void addOrUpdate(Cluster cluster) {\n        List<ReplaceableAttribute> attrs = new ArrayList<ReplaceableAttribute>();\n        Map<String, String> fieldToValueMap = cluster.getFieldToValueMap();\n        for (Map.Entry<String, String> entry : fieldToValueMap.entrySet()) {\n            attrs.add(new ReplaceableAttribute(entry.getKey(), StringUtils.left(entry.getValue(), MAX_ATTR_SIZE),\n                    true));\n        }\n        PutAttributesRequest putReqest = new PutAttributesRequest(domain, getSimpleDBItemName(cluster), attrs);\n        LOGGER.debug(String.format(\"Saving cluster %s to SimpleDB domain %s\",\n                cluster.getName(), domain));\n        this.simpleDBClient.putAttributes(putReqest);\n        LOGGER.debug(\"Successfully saved.\");\n    }\n\n\n\n    /**\n     * Gets the clusters for a list of regions. If the regions parameter is empty, returns the clusters\n     * for all regions.\n     */\n    @Override\n    public List<Cluster> getAllClusters(String... regions) {\n        return getClusters(null, regions);\n    }\n\n    @Override\n    public List<Cluster> getNonconformingClusters(String... regions) {\n        return getClusters(false, regions);\n    }\n\n    @Override\n    public Cluster getCluster(String clusterName, String region) {\n        Validate.notEmpty(clusterName);\n        Validate.notEmpty(region);\n        StringBuilder query = new StringBuilder();\n        query.append(String.format(\"select * from `%s` where cluster = '%s' and region = '%s'\",\n                domain, clusterName, region));\n\n        LOGGER.info(String.format(\"Query is to get the cluster is '%s'\", query));\n\n        List<Item> items = querySimpleDBItems(query.toString());\n        Validate.isTrue(items.size() <= 1);\n        if (items.size() == 0) {\n            LOGGER.info(String.format(\"Not found cluster with name %s in region %s\", clusterName, region));\n            return null;\n        } else {\n            Cluster cluster = null;\n            try {\n                cluster = parseCluster(items.get(0));\n            } catch (Exception e) {\n                // Ignore the item that cannot be parsed.\n                LOGGER.error(String.format(\"SimpleDB item %s cannot be parsed into a cluster.\", items.get(0)));\n            }\n            return cluster;\n        }\n    }\n\n    @Override\n    public void deleteClusters(Cluster... clusters) {\n        Validate.notNull(clusters);\n        LOGGER.info(String.format(\"Deleting %d clusters\", clusters.length));\n        for (Cluster cluster : clusters) {\n            LOGGER.info(String.format(\"Deleting cluster %s\", cluster.getName()));\n            simpleDBClient.deleteAttributes(new DeleteAttributesRequest(domain, getSimpleDBItemName(cluster)));\n            LOGGER.info(String.format(\"Successfully deleted cluster %s\", cluster.getName()));\n        }\n    }\n\n    private List<Cluster> getClusters(Boolean conforming, String... regions) {\n        Validate.notNull(regions);\n        List<Cluster> clusters = Lists.newArrayList();\n        StringBuilder query = new StringBuilder();\n        query.append(String.format(\"select * from `%s` where cluster is not null and \", domain));\n        boolean needsAnd = false;\n        if (regions.length != 0) {\n            query.append(String.format(\"region in ('%s') \", StringUtils.join(regions, \"','\")));\n            needsAnd = true;\n        }\n        if (conforming != null) {\n            if (needsAnd) {\n                query.append(\" and \");\n            }\n            query.append(String.format(\"isConforming = '%s'\", conforming));\n        }\n\n        LOGGER.info(String.format(\"Query to retrieve clusters for regions %s is '%s'\",\n                StringUtils.join(regions, \"','\"), query.toString()));\n\n        List<Item> items = querySimpleDBItems(query.toString());\n        for (Item item : items) {\n            try {\n                clusters.add(parseCluster(item));\n            } catch (Exception e) {\n                // Ignore the item that cannot be parsed.\n                LOGGER.error(String.format(\"SimpleDB item %s cannot be parsed into a cluster.\", item), e);\n            }\n        }\n        LOGGER.info(String.format(\"Retrieved %d clusters from SimpleDB in domain %s and regions %s\",\n                clusters.size(), domain, StringUtils.join(regions, \"','\")));\n        return clusters;\n    }\n\n    /**\n     * Parses a SimpleDB item into a cluster.\n     * @param item the item from SimpleDB\n     * @return the cluster for the SimpleDB item\n     */\n    protected Cluster parseCluster(Item item) {\n        Map<String, String> fieldToValue = new HashMap<String, String>();\n        for (Attribute attr : item.getAttributes()) {\n            String name = attr.getName();\n            String value = attr.getValue();\n            if (name != null && value != null) {\n                fieldToValue.put(name, value);\n            }\n        }\n        return Cluster.parseFieldToValueMap(fieldToValue);\n    }\n\n    /**\n     * Gets the unique SimpleDB item name for a cluster. The subclass can override this\n     * method to generate the item name differently.\n     * @param cluster\n     * @return the SimpleDB item name for the cluster\n     */\n    protected String getSimpleDBItemName(Cluster cluster) {\n        return String.format(\"%s-%s\", cluster.getName(), cluster.getRegion());\n    }\n\n    private List<Item> querySimpleDBItems(String query) {\n        Validate.notNull(query);\n        String nextToken = null;\n        List<Item> items = new ArrayList<Item>();\n        do {\n            SelectRequest request = new SelectRequest(query);\n            request.setNextToken(nextToken);\n            request.setConsistentRead(Boolean.TRUE);\n            SelectResult result = this.simpleDBClient.select(request);\n            items.addAll(result.getItems());\n            nextToken = result.getNextToken();\n        } while (nextToken != null);\n\n        return items;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/crawler/AWSClusterCrawler.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.crawler;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.amazonaws.services.autoscaling.model.Instance;\nimport com.amazonaws.services.autoscaling.model.SuspendedProcess;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.common.collect.Sets;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.ClusterCrawler;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * The class implementing a crawler that gets the auto scaling groups from AWS.\n */\npublic class AWSClusterCrawler implements ClusterCrawler {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(AWSClusterCrawler.class);\n\n    private static final String NS = \"simianarmy.conformity.cluster\";\n\n    /** The map from region to the aws client in the region. */\n    private final Map<String, AWSClient> regionToAwsClient = Maps.newHashMap();\n\n    private final MonkeyConfiguration cfg;\n\n    /**\n     * Instantiates a new cluster crawler.\n     *\n     * @param regionToAwsClient\n     *            the map from region to the corresponding aws client for the region\n     */\n    public AWSClusterCrawler(Map<String, AWSClient> regionToAwsClient, MonkeyConfiguration cfg) {\n        Validate.notNull(regionToAwsClient);\n        Validate.notNull(cfg);\n        for (Map.Entry<String, AWSClient> entry : regionToAwsClient.entrySet()) {\n            this.regionToAwsClient.put(entry.getKey(), entry.getValue());\n        }\n        this.cfg = cfg;\n    }\n\n    /**\n     * In this implementation, every auto scaling group is considered a cluster.\n     * @param clusterNames\n     *          the cluster names\n     * @return the list of clusters matching the names, when names are empty, return all clusters\n     */\n    @Override\n    public List<Cluster> clusters(String... clusterNames) {\n        List<Cluster> list = Lists.newArrayList();\n        for (Map.Entry<String, AWSClient> entry : regionToAwsClient.entrySet()) {\n            String region = entry.getKey();\n            AWSClient awsClient = entry.getValue();\n            Set<String> asgInstances = Sets.newHashSet();\n            LOGGER.info(String.format(\"Crawling clusters in region %s\", region));\n            for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups(clusterNames)) {\n                List<String> instances = Lists.newArrayList();\n                for (Instance instance : asg.getInstances()) {\n                    instances.add(instance.getInstanceId());\n                    asgInstances.add(instance.getInstanceId());\n                }\n                com.netflix.simianarmy.conformity.AutoScalingGroup conformityAsg =\n                        new com.netflix.simianarmy.conformity.AutoScalingGroup(\n                                asg.getAutoScalingGroupName(),\n                                instances.toArray(new String[instances.size()]));\n\n                for (SuspendedProcess sp : asg.getSuspendedProcesses()) {\n                    if (\"AddToLoadBalancer\".equals(sp.getProcessName())) {\n                        LOGGER.info(String.format(\"ASG %s is suspended: %s\", asg.getAutoScalingGroupName(),\n                                asg.getSuspendedProcesses()));\n                        conformityAsg.setSuspended(true);\n                    }\n                }\n\n                Cluster cluster = new Cluster(asg.getAutoScalingGroupName(), region, conformityAsg);\n                \n                List<TagDescription> tagDescriptions = asg.getTags();\n                for (TagDescription tagDescription : tagDescriptions) {\n                    if ( BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY.equalsIgnoreCase(tagDescription.getKey()) ) {\n                        String value = tagDescription.getValue();\n                        if (value != null) {\n                            cluster.setOwnerEmail(value);\n                        }\n                    }\n                }\n\n                updateCluster(cluster);\n                list.add(cluster);\n            }\n            //Cluster containing all solo instances\n            Set<String> instances = Sets.newHashSet();\n            for (com.amazonaws.services.ec2.model.Instance awsInstance : awsClient.describeInstances()) {\n                if (!asgInstances.contains(awsInstance.getInstanceId())) {\n                    LOGGER.info(String.format(\"Adding instance %s to soloInstances cluster.\",\n                            awsInstance.getInstanceId()));\n                    instances.add(awsInstance.getInstanceId());\n                }\n            }\n            //Only create cluster if we have solo instances.\n            if (!instances.isEmpty()) {\n                Cluster cluster = new Cluster(\"SoloInstances\", region, instances);\n                updateCluster(cluster);\n                list.add(cluster);\n            }\n        }\n        return list;\n    }\n\n    private void updateCluster(Cluster cluster) {\n        updateExcludedConformityRules(cluster);\n        cluster.setOwnerEmail(getOwnerEmailForCluster(cluster));\n        String prop = String.format(\"simianarmy.conformity.cluster.%s.optedOut\", cluster.getName());\n        if (cfg.getBoolOrElse(prop, false)) {\n            LOGGER.info(String.format(\"Cluster %s is opted out of Conformity Monkey.\", cluster.getName()));\n            cluster.setOptOutOfConformity(true);\n        } else {\n            cluster.setOptOutOfConformity(false);\n        }\n    }\n\n    /**\n     * Gets the owner email from the monkey configuration.\n     * @param cluster\n     *          the cluster\n     * @return the owner email if it is defined in the configuration, null otherwise.\n     */\n    @Override\n    public String getOwnerEmailForCluster(Cluster cluster) {\n        String prop = String.format(\"%s.%s.ownerEmail\", NS, cluster.getName());\n        String ownerEmail = cfg.getStr(prop);\n        if (ownerEmail == null) {\n            ownerEmail = cluster.getOwnerEmail();\n            if (ownerEmail == null) {\n                LOGGER.info(String.format(\"No owner email is found for cluster %s in configuration \"\n                    + \"%s or tag %s.\", cluster.getName(), prop, BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY));\n            } else {\n                LOGGER.info(String.format(\"Found owner email %s for cluster %s in tag %s.\",\n                        ownerEmail, cluster.getName(), BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY));\n                return ownerEmail;\n            }\n        } else {\n            LOGGER.info(String.format(\"Found owner email %s for cluster %s in configuration %s.\",\n                    ownerEmail, cluster.getName(), prop));\n        }\n        return ownerEmail;\n    }\n\n    @Override\n    public void updateExcludedConformityRules(Cluster cluster) {\n        String prop = String.format(\"%s.%s.excludedRules\", NS, cluster.getName());\n        String excludedRules = cfg.getStr(prop);\n        if (StringUtils.isNotBlank(excludedRules)) {\n            LOGGER.info(String.format(\"Excluded rules for cluster %s are : %s\", cluster.getName(), excludedRules));\n            cluster.excludeRules(StringUtils.split(excludedRules, \",\"));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/BasicConformityEurekaClient.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.netflix.appinfo.InstanceInfo;\nimport com.netflix.discovery.DiscoveryClient;\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * The class implementing a client to access Eureda for getting instance information that is used\n * by Conformity Monkey.\n */\npublic class BasicConformityEurekaClient implements ConformityEurekaClient {\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicConformityEurekaClient.class);\n\n    private final DiscoveryClient discoveryClient;\n\n    /**\n     * Constructor.\n     * @param discoveryClient the client to access Discovery/Eureka service.\n     */\n    public BasicConformityEurekaClient(DiscoveryClient discoveryClient) {\n        this.discoveryClient = discoveryClient;\n    }\n\n    @Override\n    public boolean hasHealthCheckUrl(String region, String instanceId) {\n        List<InstanceInfo> instanceInfos = discoveryClient.getInstancesById(instanceId);\n        for (InstanceInfo info : instanceInfos) {\n            Set<String> healthCheckUrls = info.getHealthCheckUrls();\n            if (healthCheckUrls != null && !healthCheckUrls.isEmpty()) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public boolean hasStatusUrl(String region, String instanceId) {\n        List<InstanceInfo> instanceInfos = discoveryClient.getInstancesById(instanceId);\n        for (InstanceInfo info : instanceInfos) {\n            String statusPageUrl = info.getStatusPageUrl();\n            if (!StringUtils.isEmpty(statusPageUrl)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public boolean isHealthy(String region, String instanceId) {\n        List<InstanceInfo> instanceInfos = discoveryClient.getInstancesById(instanceId);\n        if (instanceInfos.isEmpty()) {\n            LOGGER.info(String.format(\"Instance %s is not registered in Eureka in region %s.\", instanceId, region));\n            return false;\n        } else {\n            for (InstanceInfo info : instanceInfos) {\n                InstanceInfo.InstanceStatus status = info.getStatus();\n                if (!status.equals(InstanceInfo.InstanceStatus.UP)\n                        && !status.equals(InstanceInfo.InstanceStatus.STARTING)) {\n                    LOGGER.info(String.format(\"Instance %s is not healthy in Eureka with status %s.\",\n                            instanceId, status.name()));\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/ConformityEurekaClient.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\n/**\n * The interface for a client to access Eureka service to get the status of instances for Conformity Monkey.\n */\npublic interface ConformityEurekaClient {\n    /**\n     * Checks whether an instance has health check url in Eureka.\n     * @param region the region of the instance\n     * @param instanceId the instance id\n     * @return true if the instance has health check url in Eureka, false otherwise.\n     */\n    boolean hasHealthCheckUrl(String region, String instanceId);\n\n    /**\n     * Checks whether an instance has status url in Eureka.\n     * @param region the region of the instance\n     * @param instanceId the instance id\n     * @return true if the instance has status url in Eureka, false otherwise.\n     */\n    boolean hasStatusUrl(String region, String instanceId);\n\n    /**\n     * Checks whether an instance is healthy in Eureka.\n     * @param region the region of the instance\n     * @param instanceId the instance id\n     * @return true if the instance is healthy in Eureka, false otherwise.\n     */\n    boolean isHealthy(String region, String instanceId);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/CrossZoneLoadBalancing.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.netflix.simianarmy.client.MonkeyRestClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.auth.DefaultAWSCredentialsProviderChain;\nimport com.amazonaws.services.elasticloadbalancing.model.LoadBalancerAttributes;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityRule;\n\n/**\n * The class implementing a conformity rule that checks if the cross-zone load balancing is enabled \n * for all cluster ELBs.\n */\npublic class CrossZoneLoadBalancing implements ConformityRule {\n    private static final Logger LOGGER = LoggerFactory.getLogger(CrossZoneLoadBalancing.class);\n  \n    private final Map<String, AWSClient> regionToAwsClient = Maps.newHashMap();\n  \n    private AWSCredentialsProvider awsCredentialsProvider;\n  \n    private static final String RULE_NAME = \"CrossZoneLoadBalancing\";\n    private static final String REASON = \"Cross-zone load balancing is disabled\";\n  \n    /**\n     * Constructs an instance with the default AWS credentials provider chain.\n     * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain\n     */\n    public CrossZoneLoadBalancing() {\n        this(new DefaultAWSCredentialsProviderChain());\n    }\n  \n    /**\n     * Constructs an instance with the passed AWS Credential Provider.\n     * @param awsCredentialsProvider\n     */\n    public CrossZoneLoadBalancing(AWSCredentialsProvider awsCredentialsProvider) {\n        this.awsCredentialsProvider = awsCredentialsProvider;\n    }\n    \n    @Override\n    public Conformity check(Cluster cluster) {\n        Collection<String> failedComponents = Lists.newArrayList();\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            try {\n                for (String lbName : getLoadBalancerNamesForAsg(cluster.getRegion(), asg.getName())) {\n                    if (!isCrossZoneLoadBalancingEnabled(cluster.getRegion(), lbName)) {\n                        LOGGER.info(String.format(\"ELB %s in %s does not have cross-zone load balancing enabled\",\n                                    lbName, cluster.getRegion()));\n                        failedComponents.add(lbName);\n                    }\n                }\n            } catch (MonkeyRestClient.DataReadException e) {\n                LOGGER.error(String.format(\"Transient error reading ELB for %s in %s - skipping this check\",\n                             asg.getName(), cluster.getRegion()), e);\n            }\n        }\n        return new Conformity(getName(), failedComponents);\n    }\n  \n    /**\n     * Gets the cross-zone load balancing option for an ELB. Can be overridden in subclasses.\n     * @param region the region\n     * @param lbName the ELB name\n     * @return {@code true} if cross-zone load balancing is enabled\n     */\n    protected boolean isCrossZoneLoadBalancingEnabled(String region, String lbName) {\n        LoadBalancerAttributes attrs = getAwsClient(region).describeElasticLoadBalancerAttributes(lbName);\n        return attrs.getCrossZoneLoadBalancing().isEnabled();\n    }\n\n    @Override\n    public String getName() {\n      return RULE_NAME;\n    }\n  \n    @Override\n    public String getNonconformingReason() {\n      return REASON;\n    }\n\n    /**\n     * Gets the load balancer names of an ASG. Can be overridden in subclasses.\n     * @param region the region\n     * @param asgName the ASG name\n     * @return the list of load balancer names\n     */\n    protected List<String> getLoadBalancerNamesForAsg(String region, String asgName) {\n        List<com.amazonaws.services.autoscaling.model.AutoScalingGroup> asgs =\n                getAwsClient(region).describeAutoScalingGroups(asgName);\n        if (asgs.isEmpty()) {\n            LOGGER.error(String.format(\"Not found ASG with name %s\", asgName));\n            return Collections.emptyList();\n        } else {\n            return asgs.get(0).getLoadBalancerNames();\n        }\n    }\n\n    private AWSClient getAwsClient(String region) {\n        AWSClient awsClient = regionToAwsClient.get(region);\n        if (awsClient == null) {\n            awsClient = new AWSClient(region, awsCredentialsProvider);\n            regionToAwsClient.put(region, awsClient);\n        }\n        return awsClient;\n    }\n\n\n    \n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceHasHealthCheckUrl.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityRule;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\n\n/**\n * The class implementing a conformity rule that checks if all instances in a cluster has health check url\n * in Discovery/Eureka.\n */\npublic class InstanceHasHealthCheckUrl implements ConformityRule {\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasHealthCheckUrl.class);\n\n    private static final String RULE_NAME = \"InstanceHasHealthCheckUrl\";\n    private static final String REASON = \"Health check url not defined\";\n\n    private final ConformityEurekaClient conformityEurekaClient;\n\n    /**\n     * Constructor.\n     * @param conformityEurekaClient\n     *          the client to access the Discovery/Eureka service for checking the status of instances.\n     */\n    public InstanceHasHealthCheckUrl(ConformityEurekaClient conformityEurekaClient) {\n        Validate.notNull(conformityEurekaClient);\n        this.conformityEurekaClient = conformityEurekaClient;\n    }\n\n    @Override\n    public Conformity check(Cluster cluster) {\n        Collection<String> failedComponents = Lists.newArrayList();\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            if (asg.isSuspended()) {\n                continue;\n            }\n            for (String instance : asg.getInstances()) {\n                if (!conformityEurekaClient.hasHealthCheckUrl(cluster.getRegion(), instance)) {\n                    LOGGER.info(String.format(\"Instance %s does not have health check url in discovery.\",\n                            instance));\n                    failedComponents.add(instance);\n                }\n            }\n        }\n        return new Conformity(getName(), failedComponents);\n    }\n\n    @Override\n    public String getName() {\n        return RULE_NAME;\n    }\n\n    @Override\n    public String getNonconformingReason() {\n        return REASON;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceHasStatusUrl.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityRule;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\n\n/**\n * The class implementing a conformity rule that checks if all instances in a cluster has status url.\n */\npublic class InstanceHasStatusUrl implements ConformityRule {\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasStatusUrl.class);\n\n    private static final String RULE_NAME = \"InstanceHasStatusUrl\";\n    private static final String REASON = \"Status url not defined\";\n\n    private final ConformityEurekaClient conformityEurekaClient;\n\n    /**\n     * Constructor.\n     * @param conformityEurekaClient\n     *          the client to access the Discovery/Eureka service for checking the status of instances.\n     */\n    public InstanceHasStatusUrl(ConformityEurekaClient conformityEurekaClient) {\n        Validate.notNull(conformityEurekaClient);\n        this.conformityEurekaClient = conformityEurekaClient;\n    }\n\n    @Override\n    public Conformity check(Cluster cluster) {\n        Collection<String> failedComponents = Lists.newArrayList();\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            if (asg.isSuspended()) {\n                continue;\n            }\n            for (String instance : asg.getInstances()) {\n                if (!conformityEurekaClient.hasStatusUrl(cluster.getRegion(), instance)) {\n                    LOGGER.info(String.format(\"Instance %s does not have a status page url in discovery.\",\n                            instance));\n                    failedComponents.add(instance);\n                }\n            }\n        }\n        return new Conformity(getName(), failedComponents);\n    }\n\n    @Override\n    public String getName() {\n        return RULE_NAME;\n    }\n\n    @Override\n    public String getNonconformingReason() {\n        return REASON;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceInSecurityGroup.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.auth.DefaultAWSCredentialsProviderChain;\nimport com.amazonaws.services.ec2.model.GroupIdentifier;\nimport com.amazonaws.services.ec2.model.Instance;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.common.collect.Sets;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityRule;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The class implementing a conformity rule that checks whether or not all instances in a cluster are in\n * specific security groups.\n */\npublic class InstanceInSecurityGroup implements ConformityRule {\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasStatusUrl.class);\n\n    private static final String RULE_NAME = \"InstanceInSecurityGroup\";\n    private final String reason;\n\n    private final Collection<String> requiredSecurityGroupNames = Sets.newHashSet();\n\n    private AWSCredentialsProvider awsCredentialsProvider;\n\n    /**\n     * Constructor.\n     * @param requiredSecurityGroupNames\n     *      The security group names that are required to have for every instance of a cluster.\n     */\n    public InstanceInSecurityGroup(String... requiredSecurityGroupNames) {\n        this(new DefaultAWSCredentialsProviderChain(), requiredSecurityGroupNames);\n    }\n\n    /**\n     * Constructor.\n     * @param awsCredentialsProvider\n     *      The AWS credentials provider\n     * @param requiredSecurityGroupNames\n     *      The security group names that are required to have for every instance of a cluster.\n     */\n    public InstanceInSecurityGroup(AWSCredentialsProvider awsCredentialsProvider, String... requiredSecurityGroupNames)\n    {\n        this.awsCredentialsProvider = awsCredentialsProvider;\n        Validate.notNull(requiredSecurityGroupNames);\n        for (String sgName : requiredSecurityGroupNames) {\n            Validate.notNull(sgName);\n            this.requiredSecurityGroupNames.add(sgName.trim());\n        }\n        this.reason = String.format(\"Instances are not part of security groups (%s)\",\n                StringUtils.join(this.requiredSecurityGroupNames, \",\"));\n    }\n\n    @Override\n    public Conformity check(Cluster cluster) {\n        List<String> instanceIds = Lists.newArrayList();\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            instanceIds.addAll(asg.getInstances());\n        }\n        Collection<String> failedComponents = Lists.newArrayList();\n        if (instanceIds.size() != 0) {\n            Map<String, List<String>> instanceIdToSecurityGroup = getInstanceSecurityGroups(\n                    cluster.getRegion(), instanceIds.toArray(new String[instanceIds.size()]));\n\n            for (Map.Entry<String, List<String>> entry : instanceIdToSecurityGroup.entrySet()) {\n                String instanceId = entry.getKey();\n                if (!checkSecurityGroups(entry.getValue())) {\n                    LOGGER.info(String.format(\"Instance %s does not have all required security groups\", instanceId));\n                    failedComponents.add(instanceId);\n                }\n            }\n        }\n        return new Conformity(getName(), failedComponents);\n    }\n\n    @Override\n    public String getName() {\n        return RULE_NAME;\n    }\n\n    @Override\n    public String getNonconformingReason() {\n        return reason;\n    }\n\n    /**\n     * Checks whether the collection of security group names are valid. The default implementation here is to check\n     * whether the security groups contain the required security groups. The method can be overridden for different\n     * rules.\n     * @param sgNames\n     *      The collection of security group names\n     * @return\n     *      true if the security group names are valid, false otherwise.\n     */\n    protected boolean checkSecurityGroups(Collection<String> sgNames) {\n        for (String requiredSg : requiredSecurityGroupNames) {\n            if (!sgNames.contains(requiredSg)) {\n                LOGGER.info(String.format(\"Required security group %s is not found.\", requiredSg));\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Gets the security groups for a list of instance ids of the same region. The default implementation\n     * is using an AWS client. The method can be overridden in subclasses to get the security groups differently.\n     * @param region\n     *      the region of the instances\n     * @param instanceIds\n     *      the instance ids, all instances should be in the same region.\n     * @return\n     *      the map from instance id to the list of security group names the instance has\n     */\n    protected Map<String, List<String>> getInstanceSecurityGroups(String region, String... instanceIds) {\n        Map<String, List<String>> result = Maps.newHashMap();\n        if (instanceIds == null || instanceIds.length == 0) {\n            return result;\n        }\n        AWSClient awsClient = new AWSClient(region, awsCredentialsProvider);\n        for (Instance instance : awsClient.describeInstances(instanceIds)) {\n            // Ignore instances that are in VPC\n            if (StringUtils.isNotEmpty(instance.getVpcId())) {\n                LOGGER.info(String.format(\"Instance %s is in VPC and is ignored.\", instance.getInstanceId()));\n                continue;\n            }\n\n            if (!\"running\".equals(instance.getState().getName())) {\n                LOGGER.info(String.format(\"Instance %s is not running, state is %s.\",\n                        instance.getInstanceId(), instance.getState().getName()));\n                continue;\n            }\n\n            List<String> sgs = Lists.newArrayList();\n            for (GroupIdentifier groupId : instance.getSecurityGroups()) {\n                sgs.add(groupId.getGroupName());\n            }\n            result.put(instance.getInstanceId(), sgs);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceInVPC.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.auth.DefaultAWSCredentialsProviderChain;\nimport com.amazonaws.services.ec2.model.Instance;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.common.collect.Sets;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityRule;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * The class implements a conformity rule to check an instance is in a virtual private cloud.\n */\npublic class InstanceInVPC implements ConformityRule {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceInVPC.class);\n\n    private final Map<String, AWSClient> regionToAwsClient = Maps.newHashMap();\n\n    private AWSCredentialsProvider awsCredentialsProvider;\n\n    private static final String RULE_NAME = \"InstanceInVPC\";\n    private static final String REASON = \"VPC_ID not defined\";\n\n    /**\n     * Constructs an instance with the default AWS credentials provider chain.\n     * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain\n     */\n    public InstanceInVPC() {\n        this(new DefaultAWSCredentialsProviderChain());\n    }\n\n    /**\n     * Constructs an instance with the passed AWS credentials provider.\n     * @param awsCredentialsProvider\n     *      The AWS credentials provider\n     */\n    public InstanceInVPC(AWSCredentialsProvider awsCredentialsProvider) {\n        this.awsCredentialsProvider = awsCredentialsProvider;\n    }\n\n    @Override\n    public Conformity check(Cluster cluster) {\n        Collection<String> failedComponents = Lists.newArrayList();\n        //check all instances\n        Set<String> failedInstances = checkInstancesInVPC(cluster.getRegion(), cluster.getSoloInstances());\n        failedComponents.addAll(failedInstances);\n        //check asg instances\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            if (asg.isSuspended()) {\n                continue;\n            }\n            Set<String> asgFailedInstances = checkInstancesInVPC(cluster.getRegion(), asg.getInstances());\n            failedComponents.addAll(asgFailedInstances);\n        }\n        return new Conformity(getName(), failedComponents);\n    }\n\n    @Override\n    public String getName() {\n        return RULE_NAME;\n    }\n\n    @Override\n    public String getNonconformingReason() {\n        return REASON;\n    }\n\n    private AWSClient getAwsClient(String region) {\n        AWSClient awsClient = regionToAwsClient.get(region);\n        if (awsClient == null) {\n            awsClient = new AWSClient(region, awsCredentialsProvider);\n            regionToAwsClient.put(region, awsClient);\n        }\n        return awsClient;\n    }\n\n    private Set<String> checkInstancesInVPC(String region, Collection<String> instances) {\n        Set<String> failedInstances = Sets.newHashSet();\n        for (String instanceId : instances) {\n            for (Instance awsInstance : getAWSInstances(region, instanceId)) {\n                if (awsInstance.getVpcId() == null) {\n                    LOGGER.info(String.format(\"Instance %s is not in a virtual private cloud\", instanceId));\n                    failedInstances.add(instanceId);\n                }\n            }\n        }\n        return failedInstances;\n    }\n\n    /**\n     * Gets the list of AWS instances. Can be overridden\n     * @param region the region\n     * @param instanceId the instance id.\n     * @return the list of the AWS instances with the given id.\n     */\n    protected List<Instance> getAWSInstances(String region, String instanceId) {\n        AWSClient awsClient = getAwsClient(region);\n        return awsClient.describeInstances(instanceId);\n    }\n}"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceIsHealthyInEureka.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityRule;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\n\n/**\n * The class implements a conformity rule to check if all instances in the cluster are healthy in Discovery.\n */\npublic class InstanceIsHealthyInEureka implements ConformityRule {\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceIsHealthyInEureka.class);\n\n    private static final String RULE_NAME = \"InstanceIsHealthyInEureka\";\n    private static final String REASON = \"Instances are not 'UP' in Eureka.\";\n\n    private final ConformityEurekaClient conformityEurekaClient;\n\n    /**\n     * Constructor.\n     * @param conformityEurekaClient\n     *          the client to access the Discovery/Eureka service for checking the status of instances.\n     */\n    public InstanceIsHealthyInEureka(ConformityEurekaClient conformityEurekaClient) {\n        Validate.notNull(conformityEurekaClient);\n        this.conformityEurekaClient = conformityEurekaClient;\n    }\n\n    @Override\n    public Conformity check(Cluster cluster) {\n        Collection<String> failedComponents = Lists.newArrayList();\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            // ignore suspended ASGs\n            if (asg.isSuspended()) {\n                LOGGER.info(String.format(\"ASG %s is suspended, ignore.\", asg.getName()));\n                continue;\n            }\n            for (String instance : asg.getInstances()) {\n                if (!conformityEurekaClient.isHealthy(cluster.getRegion(), instance)) {\n                    LOGGER.info(String.format(\"Instance %s is not healthy in Eureka.\", instance));\n                    failedComponents.add(instance);\n                }\n            }\n        }\n        return new Conformity(getName(), failedComponents);\n    }\n\n    @Override\n    public String getName() {\n        return RULE_NAME;\n    }\n\n    @Override\n    public String getNonconformingReason() {\n        return REASON;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceTooOld.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.auth.DefaultAWSCredentialsProviderChain;\nimport com.amazonaws.services.ec2.model.Instance;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityRule;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The class implementing a conformity rule that checks if there are instances that are older than certain days.\n * Instances are not considered to be permanent in the cloud, so sometimes having too old instances could indicate\n * potential issues.\n */\npublic class InstanceTooOld implements ConformityRule {\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasStatusUrl.class);\n\n    private static final String RULE_NAME = \"InstanceTooOld\";\n    private final String reason;\n    private final int instanceAgeThreshold;\n\n    private AWSCredentialsProvider awsCredentialsProvider;\n\n    /**\n     * Constructor.\n     * @param instanceAgeThreshold\n     *      The age in days that makes an instance be considered too old.\n     */\n    public InstanceTooOld(int instanceAgeThreshold) {\n        this(new DefaultAWSCredentialsProviderChain(), instanceAgeThreshold);\n    }\n\n    /**\n     * Constructor.\n     * @param awsCredentialsProvider\n     *      The AWS credentials provider\n     * @param instanceAgeThreshold\n     *      The age in days that makes an instance be considered too old.\n     */\n    public InstanceTooOld(AWSCredentialsProvider awsCredentialsProvider, int instanceAgeThreshold) {\n        this.awsCredentialsProvider = awsCredentialsProvider;\n        Validate.isTrue(instanceAgeThreshold > 0);\n        this.instanceAgeThreshold = instanceAgeThreshold;\n        this.reason = String.format(\"Instances are older than %d days\", instanceAgeThreshold);\n    }\n\n    @Override\n    public Conformity check(Cluster cluster) {\n        List<String> instanceIds = Lists.newArrayList();\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            instanceIds.addAll(asg.getInstances());\n        }\n        Map<String, Long> instanceIdToLaunchTime = getInstanceLaunchTimes(\n                cluster.getRegion(), instanceIds.toArray(new String[instanceIds.size()]));\n\n        Collection<String> failedComponents = Lists.newArrayList();\n        long creationTimeThreshold = DateTime.now().minusDays(instanceAgeThreshold).getMillis();\n        for (Map.Entry<String, Long> entry : instanceIdToLaunchTime.entrySet()) {\n            String instanceId = entry.getKey();\n            if (creationTimeThreshold > entry.getValue()) {\n                LOGGER.info(String.format(\"Instance %s was created more than %d days ago\",\n                        instanceId, instanceAgeThreshold));\n                failedComponents.add(instanceId);\n            }\n        }\n        return new Conformity(getName(), failedComponents);\n    }\n\n    @Override\n    public String getName() {\n        return RULE_NAME;\n    }\n\n    @Override\n    public String getNonconformingReason() {\n        return reason;\n    }\n\n    /**\n     * Gets the launch time (in milliseconds) for a list of instance ids of the same region. The default\n     * implementation is using an AWS client. The method can be overridden in subclasses to get the instance\n     * launch times differently.\n     * @param region\n     *      the region of the instances\n     * @param instanceIds\n     *      the instance ids, all instances should be in the same region.\n     * @return\n     *      the map from instance id to the launch time in milliseconds\n     */\n    protected Map<String, Long> getInstanceLaunchTimes(String region, String... instanceIds) {\n        Map<String, Long> result = Maps.newHashMap();\n        if (instanceIds == null || instanceIds.length == 0) {\n            return result;\n        }\n        AWSClient awsClient = new AWSClient(region, awsCredentialsProvider);\n        for (Instance instance : awsClient.describeInstances(instanceIds)) {\n            if (instance.getLaunchTime() != null) {\n                result.put(instance.getInstanceId(), instance.getLaunchTime().getTime());\n            } else {\n                LOGGER.warn(String.format(\"No launch time found for instance %s\", instance.getInstanceId()));\n            }\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/conformity/rule/SameZonesInElbAndAsg.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.auth.DefaultAWSCredentialsProviderChain;\nimport com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityRule;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The class implementing a conformity rule that checks if the zones in ELB and ASG are the same.\n */\npublic class SameZonesInElbAndAsg implements ConformityRule {\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasStatusUrl.class);\n\n    private final Map<String, AWSClient> regionToAwsClient = Maps.newHashMap();\n\n    private AWSCredentialsProvider awsCredentialsProvider;\n\n    private static final String RULE_NAME = \"SameZonesInElbAndAsg\";\n    private static final String REASON = \"Availability zones of ELB and ASG are different\";\n\n    /**\n     * Constructs an instance with the default AWS credentials provider chain.\n     * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain\n     */\n    public SameZonesInElbAndAsg() {\n        this(new DefaultAWSCredentialsProviderChain());\n    }\n\n    /**\n     * Constructs an instance with the passed AWS Credential Provider.\n     * @param awsCredentialsProvider\n     */\n    public SameZonesInElbAndAsg(AWSCredentialsProvider awsCredentialsProvider) {\n        this.awsCredentialsProvider = awsCredentialsProvider;\n    }\n\n    @Override\n    public Conformity check(Cluster cluster) {\n        List<String> asgNames = Lists.newArrayList();\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            asgNames.add(asg.getName());\n        }\n        Collection<String> failedComponents = Lists.newArrayList();\n        for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) {\n            List<String> asgZones = getAvailabilityZonesForAsg(cluster.getRegion(), asg.getName());\n            for (String lbName : getLoadBalancerNamesForAsg(cluster.getRegion(), asg.getName())) {\n                List<String> lbZones = getAvailabilityZonesForLoadBalancer(cluster.getRegion(), lbName);\n                if (!haveSameZones(asgZones, lbZones)) {\n                    LOGGER.info(String.format(\"ASG %s and ELB %s do not have the same availability zones\",\n                            asgZones, lbZones));\n                    failedComponents.add(lbName);\n                }\n            }\n        }\n        return new Conformity(getName(), failedComponents);\n    }\n\n    @Override\n    public String getName() {\n        return RULE_NAME;\n    }\n\n    @Override\n    public String getNonconformingReason() {\n        return REASON;\n    }\n\n    /**\n     * Gets the load balancer names of an ASG. Can be overridden in subclasses.\n     * @param region the region\n     * @param asgName the ASG name\n     * @return the list of load balancer names\n     */\n    protected List<String> getLoadBalancerNamesForAsg(String region, String asgName) {\n        List<com.amazonaws.services.autoscaling.model.AutoScalingGroup> asgs =\n                getAwsClient(region).describeAutoScalingGroups(asgName);\n        if (asgs.isEmpty()) {\n            LOGGER.error(String.format(\"Not found ASG with name %s\", asgName));\n            return Collections.emptyList();\n        } else {\n            return asgs.get(0).getLoadBalancerNames();\n        }\n    }\n\n    /**\n     * Gets the list of availability zones for an ASG. Can be overridden in subclasses.\n     * @param region the region\n     * @param asgName the ASG name.\n     * @return the list of the availability zones that the ASG has.\n     */\n    protected List<String> getAvailabilityZonesForAsg(String region, String asgName) {\n        List<com.amazonaws.services.autoscaling.model.AutoScalingGroup> asgs =\n                getAwsClient(region).describeAutoScalingGroups(asgName);\n        if (asgs.isEmpty()) {\n            LOGGER.error(String.format(\"Not found ASG with name %s\", asgName));\n            return null;\n        } else {\n            return asgs.get(0).getAvailabilityZones();\n        }\n    }\n\n    /**\n     * Gets the list of availability zones for a load balancer. Can be overridden in subclasses.\n     * @param region the region\n     * @param lbName the load balancer name.\n     * @return the list of the availability zones that the load balancer has.\n     */\n    protected List<String> getAvailabilityZonesForLoadBalancer(String region, String lbName) {\n        List<LoadBalancerDescription> lbs =\n                getAwsClient(region).describeElasticLoadBalancers(lbName);\n        if (lbs.isEmpty()) {\n            LOGGER.error(String.format(\"Not found load balancer with name %s\", lbName));\n            return null;\n        } else {\n            return lbs.get(0).getAvailabilityZones();\n        }\n    }\n\n    private AWSClient getAwsClient(String region) {\n        AWSClient awsClient = regionToAwsClient.get(region);\n        if (awsClient == null) {\n            awsClient = new AWSClient(region, awsCredentialsProvider);\n            regionToAwsClient.put(region, awsClient);\n        }\n        return awsClient;\n    }\n\n\n    private boolean haveSameZones(List<String> zones1, List<String> zones2) {\n        if (zones1 == null || zones2 == null) {\n            return true;\n        }\n        if (zones1.size() != zones1.size()) {\n            return false;\n        }\n        for (String zone : zones1) {\n            if (!zones2.contains(zone)) {\n                return false;\n            }\n        }\n        for (String zone : zones2) {\n            if (!zones1.contains(zone)) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/ASGJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor;\n\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.AbstractJanitor;\n\n/**\n * The Janitor responsible for ASG cleanup.\n */\npublic class ASGJanitor extends AbstractJanitor {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJanitor.class);\n\n    private final AWSClient awsClient;\n\n    /**\n     * Constructor.\n     * @param awsClient the AWS client\n     * @param ctx the context\n     */\n    public ASGJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {\n        super(ctx, AWSResourceType.ASG);\n        Validate.notNull(awsClient);\n        this.awsClient = awsClient;\n    }\n\n    @Override\n    protected void postMark(Resource resource) {\n    }\n\n    @Override\n    protected void cleanup(Resource resource) {\n        LOGGER.info(String.format(\"Deleting ASG %s\", resource.getId()));\n        awsClient.deleteAutoScalingGroup(resource.getId());\n    }\n\n    @Override\n    protected void postCleanup(Resource resource) {\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/EBSSnapshotJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor;\n\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.AbstractJanitor;\n\n/**\n * The Janitor responsible for EBS snapshot cleanup.\n */\npublic class EBSSnapshotJanitor extends AbstractJanitor {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EBSSnapshotJanitor.class);\n\n    private final AWSClient awsClient;\n\n    /**\n     * Constructor.\n     * @param awsClient the AWS client\n     * @param ctx the context\n     */\n    public EBSSnapshotJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {\n        super(ctx, AWSResourceType.EBS_SNAPSHOT);\n        Validate.notNull(awsClient);\n        this.awsClient = awsClient;\n    }\n\n    @Override\n    protected void postMark(Resource resource) {\n    }\n\n    @Override\n    protected void cleanup(Resource resource) {\n        LOGGER.info(String.format(\"Deleting EBS snapshot %s\", resource.getId()));\n        awsClient.deleteSnapshot(resource.getId());\n    }\n\n    @Override\n    protected void postCleanup(Resource resource) {\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/EBSVolumeJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor;\n\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.AbstractJanitor;\n\n/**\n * The Janitor responsible for EBS volume cleanup.\n */\npublic class EBSVolumeJanitor extends AbstractJanitor {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EBSVolumeJanitor.class);\n\n    private final AWSClient awsClient;\n\n    /**\n     * Constructor.\n     * @param awsClient the AWS client\n     * @param ctx the context\n     */\n    public EBSVolumeJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {\n        super(ctx, AWSResourceType.EBS_VOLUME);\n        Validate.notNull(awsClient);\n        this.awsClient = awsClient;\n    }\n\n    @Override\n    protected void postMark(Resource resource) {\n    }\n\n    @Override\n    protected void cleanup(Resource resource) {\n        LOGGER.info(String.format(\"Deleting EBS volume %s\", resource.getId()));\n        awsClient.deleteVolume(resource.getId());\n    }\n\n    @Override\n    protected void postCleanup(Resource resource) {\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/ELBJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.AbstractJanitor;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * The Janitor responsible for elastic load balancer cleanup.\n */\npublic class ELBJanitor extends AbstractJanitor {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ELBJanitor.class);\n\n    private final AWSClient awsClient;\n\n    /**\n     * Constructor.\n     * @param awsClient the AWS client\n     * @param ctx the context\n     */\n    public ELBJanitor(AWSClient awsClient, Context ctx) {\n        super(ctx, AWSResourceType.ELB);\n        Validate.notNull(awsClient);\n        this.awsClient = awsClient;\n    }\n\n    @Override\n    protected void postMark(Resource resource) {\n    }\n\n    @Override\n    protected void cleanup(Resource resource) {\n        LOGGER.info(String.format(\"Deleting ELB %s\", resource.getId()));\n        awsClient.deleteElasticLoadBalancer(resource.getId());\n\n        // delete any DNS records attached to this ELB\n        String dnsNames = resource.getAdditionalField(\"referencedDNS\");\n        String dnsTypes = resource.getAdditionalField(\"referencedDNSTypes\");\n        String dnsZones = resource.getAdditionalField(\"referencedDNSZones\");\n        if (StringUtils.isNotBlank(dnsNames) && StringUtils.isNotBlank(dnsTypes) && StringUtils.isNotBlank(dnsZones)) {\n            String[] dnsNamesSplit = StringUtils.split(dnsNames,',');\n            String[] dnsTypesSplit = StringUtils.split(dnsTypes,',');\n            String[] dnsZonesSplit = StringUtils.split(dnsZones,',');\n\n            if (dnsNamesSplit.length != dnsTypesSplit.length) {\n                LOGGER.error(String.format(\"DNS Name count does not match DNS Type count, aborting DNS delete for ELB %s\"), resource.getId());\n                LOGGER.error(String.format(\"DNS Names found but not deleted: %s for ELB %s\"), dnsNames, resource.getId());\n                return;\n            }\n\n            if (dnsNamesSplit.length != dnsZonesSplit.length) {\n                LOGGER.error(String.format(\"DNS Name count does not match DNS Zone count, aborting DNS delete for ELB %s\"), resource.getId());\n                LOGGER.error(String.format(\"DNS Names found but not deleted: %s for ELB %s\"), dnsNames, resource.getId());\n                return;\n            }\n\n            for(int i=0; i<dnsNamesSplit.length; i++) {\n                LOGGER.info(String.format(\"Deleting DNS Record %s for ELB %s of type %s in zone %s\", dnsNamesSplit[i], resource.getId(), dnsTypesSplit[i], dnsZonesSplit[i]));\n                awsClient.deleteDNSRecord(dnsNamesSplit[i], dnsTypesSplit[i], dnsZonesSplit[i]);\n            }\n        }\n    }\n\n    @Override\n    protected void postCleanup(Resource resource) {\n        try {\n            Thread.sleep(5000);\n        } catch (InterruptedException e) {\n            LOGGER.warn(\"Post-cleanup sleep was interrupted\", e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/ImageJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.AbstractJanitor;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * The Janitor responsible for launch configuration cleanup.\n */\npublic class ImageJanitor extends AbstractJanitor {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ImageJanitor.class);\n\n    private final AWSClient awsClient;\n\n    /**\n     * Constructor.\n     * @param awsClient the AWS client\n     * @param ctx the context\n     */\n    public ImageJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {\n        super(ctx, AWSResourceType.IMAGE);\n        Validate.notNull(awsClient);\n        this.awsClient = awsClient;\n    }\n\n    @Override\n    protected void postMark(Resource resource) {\n    }\n\n    @Override\n    protected void cleanup(Resource resource) {\n        LOGGER.info(String.format(\"Deleting image %s\", resource.getId()));\n        awsClient.deleteImage(resource.getId());\n    }\n\n    @Override\n    protected void postCleanup(Resource resource) {\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/InstanceJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor;\n\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.AbstractJanitor;\n\n/**\n * The Janitor responsible for auto scaling instance cleanup.\n */\npublic class InstanceJanitor extends AbstractJanitor {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceJanitor.class);\n\n    private final AWSClient awsClient;\n\n    /**\n     * Constructor.\n     * @param awsClient the AWS client\n     * @param ctx the context\n     */\n    public InstanceJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {\n        super(ctx, AWSResourceType.INSTANCE);\n        Validate.notNull(awsClient);\n        this.awsClient = awsClient;\n    }\n\n    @Override\n    protected void postMark(Resource resource) {\n    }\n\n    @Override\n    protected void cleanup(Resource resource) {\n        LOGGER.info(String.format(\"Terminating instance %s\", resource.getId()));\n        awsClient.terminateInstance(resource.getId());\n    }\n\n    @Override\n    protected void postCleanup(Resource resource) {\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/LaunchConfigJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.AbstractJanitor;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * The Janitor responsible for launch configuration cleanup.\n */\npublic class LaunchConfigJanitor extends AbstractJanitor {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(LaunchConfigJanitor.class);\n\n    private final AWSClient awsClient;\n\n    /**\n     * Constructor.\n     * @param awsClient the AWS client\n     * @param ctx the context\n     */\n    public LaunchConfigJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) {\n        super(ctx, AWSResourceType.LAUNCH_CONFIG);\n        Validate.notNull(awsClient);\n        this.awsClient = awsClient;\n    }\n\n    @Override\n    protected void postMark(Resource resource) {\n    }\n\n    @Override\n    protected void cleanup(Resource resource) {\n        LOGGER.info(String.format(\"Deleting launch configuration %s\", resource.getId()));\n        awsClient.deleteLaunchConfiguration(resource.getId());\n    }\n\n    @Override\n    protected void postCleanup(Resource resource) {\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/RDSJanitorResourceTracker.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor;\n\nimport com.amazonaws.AmazonClientException;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.Resource.CleanupState;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.janitor.JanitorResourceTracker;\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.core.RowMapper;\n\nimport java.io.IOException;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.sql.Types;\nimport java.util.*;\n\n/**\n * The JanitorResourceTracker implementation in AWS RDS.\n */\npublic class RDSJanitorResourceTracker implements JanitorResourceTracker {\n\n    /** The Constant LOGGER. */\n    public static final Logger LOGGER = LoggerFactory.getLogger(RDSJanitorResourceTracker.class);\n\n    /** The table. */\n    private final String table;\n    \n    /** the jdbcTemplate  */\n    JdbcTemplate jdbcTemplate = null;\n    \n    /**\n     * Instantiates a new RDS janitor resource tracker.\n     *\n     */\n    public RDSJanitorResourceTracker(String dbDriver, String dbUser,\n\t\t\tString dbPass, String dbUrl, String dbTable) {\n\t\tHikariDataSource dataSource = new HikariDataSource();\n\t\tdataSource.setDriverClassName(dbDriver);\n\t\tdataSource.setJdbcUrl(dbUrl);\n\t\tdataSource.setUsername(dbUser);\n\t\tdataSource.setPassword(dbPass);\n\t\tdataSource.setMaximumPoolSize(2);\n    \tthis.jdbcTemplate = new JdbcTemplate(dataSource);\n    \tthis.table = dbTable;\n\t}\n    \n    /**\n     * Instantiates a new RDS janitor resource tracker.  This constructor is intended\n     * for unit testing.\n     *\n     */\n    public RDSJanitorResourceTracker(JdbcTemplate jdbcTemplate, String table) {\n    \tthis.jdbcTemplate = jdbcTemplate;\n    \tthis.table = table;\n    }\n    \n    public JdbcTemplate getJdbcTemplate() {\n\t\treturn jdbcTemplate;\n\t}\n\n    public Object value(String value) {\n    \treturn value == null ? Types.NULL : value;\n    }\n\n    public Object value(Date value) {\n    \treturn value == null ? Types.NULL : value.getTime();\n    }\n\n\tpublic Object value(boolean value) {\n\t\treturn new Boolean(value).toString();\n\t}\n\n\tpublic Object emailValue(String email) {\n\t\tif (StringUtils.isBlank(email)) return Types.NULL;\n\t\tif (email.equals(\"0\")) return Types.NULL;\n\t\treturn email;\n\t}\n\n\t/** {@inheritDoc} */\n    @Override\n    public void addOrUpdate(Resource resource) {\n    \tResource orig = getResource(resource.getId(), resource.getRegion());\n        LOGGER.debug(String.format(\"Saving resource %s to RDB table %s in region %s\", resource.getId(), table, resource.getRegion()));\n    \tString json;\n\t\ttry {\n\t\t\tjson = new ObjectMapper().writeValueAsString(additionalFieldsAsMap(resource));\n\t\t} catch (JsonProcessingException e) {\n\t\t\tLOGGER.error(\"ERROR generating additional field JSON when saving resource \" + resource.getId(), e);\n\t\t\treturn;\n\t\t}\n\n    \tif (orig == null) {\n    \t\tStringBuilder sb = new StringBuilder();\n    \t\tsb.append(\"insert into \").append(table);\n    \t\tsb.append(\" (\");\n    \t\tsb.append(AWSResource.FIELD_RESOURCE_ID).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_RESOURCE_TYPE).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_REGION).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_OWNER_EMAIL).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_DESCRIPTION).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_STATE).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_TERMINATION_REASON).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_EXPECTED_TERMINATION_TIME).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_ACTUAL_TERMINATION_TIME).append(\",\");\n\t\t\tsb.append(AWSResource.FIELD_NOTIFICATION_TIME).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_LAUNCH_TIME).append(\",\");\n    \t\tsb.append(AWSResource.FIELD_MARK_TIME).append(\",\");\n\t\t\tsb.append(AWSResource.FIELD_OPT_OUT_OF_JANITOR).append(\",\");\n    \t\tsb.append(\"additionalFields\").append(\") values (?,?,?,?,?,?,?,?,?,?,?,?,?,?)\");\n\n            LOGGER.debug(String.format(\"Insert statement is '%s'\", sb));\n    \t\tint updated = this.jdbcTemplate.update(sb.toString(),\n    \t\t\t\t\t\t\t\t resource.getId(),\n    \t\t\t\t\t\t\t\t value(resource.getResourceType().toString()),\n    \t\t\t\t\t\t\t\t value(resource.getRegion()),\n    \t\t\t\t\t\t\t\t emailValue(resource.getOwnerEmail()),\n    \t\t\t\t\t\t\t\t value(resource.getDescription()),\n    \t\t\t\t\t\t\t\t value(resource.getState().toString()),\n    \t\t\t\t\t\t\t\t value(resource.getTerminationReason()),\n    \t\t\t\t\t\t\t\t value(resource.getExpectedTerminationTime()),\n    \t\t\t\t\t\t\t\t value(resource.getActualTerminationTime()),\n\t\t\t\t\t                 value(resource.getNotificationTime()),\n    \t\t\t\t\t\t\t\t value(resource.getLaunchTime()),\n    \t\t\t\t\t\t\t\t value(resource.getMarkTime()),\n\t\t\t\t  \t\t\t\t\t value(resource.isOptOutOfJanitor()),\n    \t\t\t\t\t\t\t\t json);\n            LOGGER.debug(String.format(\"%d rows inserted\", updated));\n    \t} else {\n    \t\tStringBuilder sb = new StringBuilder();\n    \t\tsb.append(\"update \").append(table).append(\" set \");\n    \t\tsb.append(AWSResource.FIELD_RESOURCE_TYPE).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_REGION).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_OWNER_EMAIL).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_DESCRIPTION).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_STATE).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_TERMINATION_REASON).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_EXPECTED_TERMINATION_TIME).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_ACTUAL_TERMINATION_TIME).append(\"=?,\");\n\t\t\tsb.append(AWSResource.FIELD_NOTIFICATION_TIME).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_LAUNCH_TIME).append(\"=?,\");\n    \t\tsb.append(AWSResource.FIELD_MARK_TIME).append(\"=?,\");\n\t\t\tsb.append(AWSResource.FIELD_OPT_OUT_OF_JANITOR).append(\"=?,\");\n    \t\tsb.append(\"additionalFields\").append(\"=? where \");\n    \t\tsb.append(AWSResource.FIELD_RESOURCE_ID).append(\"=? and \");\n\t\t\tsb.append(AWSResource.FIELD_REGION).append(\"=?\");\n\n            LOGGER.debug(String.format(\"Update statement is '%s'\", sb));\n    \t\tint updated = this.jdbcTemplate.update(sb.toString(),\n    \t\t\t\t\t\t\t\t resource.getResourceType().toString(),\n    \t\t\t\t\t\t\t\t value(resource.getRegion()),\n\t\t\t\t\t                 emailValue(resource.getOwnerEmail()),\n    \t\t\t\t\t\t\t\t value(resource.getDescription()),\n    \t\t\t\t\t\t\t\t value(resource.getState().toString()),\n    \t\t\t\t\t\t\t\t value(resource.getTerminationReason()),\n    \t\t\t\t\t\t\t\t value(resource.getExpectedTerminationTime()),\n    \t\t\t\t\t\t\t\t value(resource.getActualTerminationTime()),\n\t\t\t\t\t                 value(resource.getNotificationTime()),\n    \t\t\t\t\t\t\t\t value(resource.getLaunchTime()),\n    \t\t\t\t\t\t\t\t value(resource.getMarkTime()),\n\t\t\t\t\t                 value(resource.isOptOutOfJanitor()),\n    \t\t\t\t\t\t\t\t json,\n    \t\t\t\t\t\t\t\t resource.getId(),\n\t\t\t\t\t\t  \t\t\t resource.getRegion());\n            LOGGER.debug(String.format(\"%d rows updated\", updated));\n    \t}\n    \tLOGGER.debug(\"Successfully saved.\");\n    }\n\n\t/**\n     * Returns a list of AWSResource objects. You need to override this method if more\n     * specific resource types (e.g. subtypes of AWSResource) need to be obtained from\n     * the Database.\n     */\n    @Override\n    public List<Resource> getResources(ResourceType resourceType, CleanupState state, String resourceRegion) {\n        Validate.notEmpty(resourceRegion);\n        StringBuilder query = new StringBuilder();\n        ArrayList<String> args = new ArrayList<>();\n        query.append(String.format(\"select * from %s where \", table));\n        if (resourceType != null) {\n            query.append(\"resourceType=? and \");\n            args.add(resourceType.toString());\n        }\n        if (state != null) {\n            query.append(\"state=? and \");\n            args.add(state.toString());\n        }\n        query.append(\"region=?\");\n        args.add(resourceRegion);\n\n        LOGGER.debug(String.format(\"Query is '%s'\", query));\n        List<Resource> resources = jdbcTemplate.query(query.toString(), args.toArray(), new RowMapper<Resource>() {\n            public Resource mapRow(ResultSet rs, int rowNum) throws SQLException {\n            \treturn mapResource(rs);                \n            }             \n        });                \n        return resources;\n    }\n    \n    private Resource mapResource(ResultSet rs) throws SQLException {\n    \tString json = rs.getString(\"additionalFields\");\n    \tResource resource = null;\n    \ttry {\n    \t\t// put additional fields\n    \t\tMap<String, String> map = new HashMap<>();\n    \t\tif (json != null) {\n            \tTypeReference<HashMap<String,String>> typeRef = new TypeReference<HashMap<String,String>>() {};    \t\t\n        \t\tmap = new ObjectMapper().readValue(json, typeRef);\n    \t\t}\n    \t\t\n    \t\t// put everything else\n    \t\tmap.put(AWSResource.FIELD_RESOURCE_ID, rs.getString(AWSResource.FIELD_RESOURCE_ID));\n    \t\tmap.put(AWSResource.FIELD_RESOURCE_TYPE, rs.getString(AWSResource.FIELD_RESOURCE_TYPE));\n    \t\tmap.put(AWSResource.FIELD_REGION, rs.getString(AWSResource.FIELD_REGION));\n    \t\tmap.put(AWSResource.FIELD_DESCRIPTION, rs.getString(AWSResource.FIELD_DESCRIPTION));\n    \t\tmap.put(AWSResource.FIELD_STATE, rs.getString(AWSResource.FIELD_STATE));\n    \t\tmap.put(AWSResource.FIELD_TERMINATION_REASON, rs.getString(AWSResource.FIELD_TERMINATION_REASON));\n\t\t\tmap.put(AWSResource.FIELD_OPT_OUT_OF_JANITOR, rs.getString(AWSResource.FIELD_OPT_OUT_OF_JANITOR));\n\n\t\t\tString email = rs.getString(AWSResource.FIELD_OWNER_EMAIL);\n\t\t\tif (StringUtils.isBlank(email) || email.equals(\"0\")) {\n\t\t\t\temail = null;\n\t\t\t}\n\t\t\tmap.put(AWSResource.FIELD_OWNER_EMAIL, email);\n\n    \t\tString expectedTerminationTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_EXPECTED_TERMINATION_TIME));\n    \t\tString actualTerminationTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_ACTUAL_TERMINATION_TIME));\n\t\t\tString notificationTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_NOTIFICATION_TIME));\n    \t\tString launchTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_LAUNCH_TIME));\n    \t\tString markTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_MARK_TIME));\n\n    \t\tif (expectedTerminationTime != null) {\n    \t\t\tmap.put(AWSResource.FIELD_EXPECTED_TERMINATION_TIME, expectedTerminationTime);\n    \t\t}\n    \t\tif (actualTerminationTime != null) {\n    \t\t\tmap.put(AWSResource.FIELD_ACTUAL_TERMINATION_TIME, actualTerminationTime);\n    \t\t}\n\t\t\tif (notificationTime != null) {\n\t\t\t\tmap.put(AWSResource.FIELD_NOTIFICATION_TIME, notificationTime);\n\t\t\t}\n    \t\tif (launchTime != null) {\n    \t\t\tmap.put(AWSResource.FIELD_LAUNCH_TIME, launchTime);\n    \t\t}\n    \t\tif (markTime != null) {\n    \t\t\tmap.put(AWSResource.FIELD_MARK_TIME, markTime);\n    \t\t}\n\n    \t\tresource = AWSResource.parseFieldtoValueMap(map);\n    \t}catch(IOException ie) {\n    \t\tString msg = \"Error parsing resource from result set\";\n    \t\tLOGGER.error(msg, ie);\n    \t\tthrow new SQLException(msg);\n    \t}    \t\n        return resource;\n    }\n\n\tprivate String millisToFormattedDate(String millisStr) {\n\t\tString datetime = null;\n\t\ttry {\n\t\t\tlong millis = Long.parseLong(millisStr);\n\t\t\tdatetime = AWSResource.DATE_FORMATTER.print(millis);\n\t\t} catch(NumberFormatException nfe) {\n\t\t\tLOGGER.error(String.format(\"Error parsing datetime %s when reading from RDS\", millisStr));\n\t\t}\n\t\treturn datetime;\n\t}\n\n    @Override\n    public Resource getResource(String resourceId) {\n        Validate.notEmpty(resourceId);\n        StringBuilder query = new StringBuilder();\n        query.append(String.format(\"select * from %s where resourceId=?\", table));\n\n        LOGGER.debug(String.format(\"Query is '%s'\", query));\n        List<Resource> resources = jdbcTemplate.query(query.toString(), new String[]{resourceId}, new RowMapper<Resource>() {\n            public Resource mapRow(ResultSet rs, int rowNum) throws SQLException {\n            \treturn mapResource(rs);                \n            }             \n        });       \n        \n        Resource resource = null;\n        Validate.isTrue(resources.size() <= 1);\n        if (resources.size() == 0) {\n            LOGGER.info(String.format(\"Not found resource with id %s\", resourceId));\n        } else {\n        \tresource = resources.get(0);\n        }\n        return resource;\n    }\n\n\t@Override\n\tpublic Resource getResource(String resourceId, String region) {\n\t\tValidate.notEmpty(resourceId);\n\t\tValidate.notEmpty(region);\n\t\tStringBuilder query = new StringBuilder();\n\t\tquery.append(String.format(\"select * from %s where resourceId=? and region=?\", table));\n\n\t\tLOGGER.debug(String.format(\"Query is '%s'\", query));\n\t\tList<Resource> resources = jdbcTemplate.query(query.toString(), new String[]{resourceId,region}, new RowMapper<Resource>() {\n\t\t\tpublic Resource mapRow(ResultSet rs, int rowNum) throws SQLException {\n\t\t\t\treturn mapResource(rs);\n\t\t\t}\n\t\t});\n\n\t\tResource resource = null;\n\t\tValidate.isTrue(resources.size() <= 1);\n\t\tif (resources.size() == 0) {\n\t\t\tLOGGER.info(String.format(\"Not found resource with id %s\", resourceId));\n\t\t} else {\n\t\t\tresource = resources.get(0);\n\t\t}\n\t\treturn resource;\n\t}\n\n\t/**\n     * Creates the RDS table, if it does not already exist.\n     */\n    public void init() {\n        try {\n            LOGGER.info(\"Creating RDS table: {}\", table);\n            String sql = String.format(\"create table if not exists %s (\"\n                                     + \" %s varchar(255), \"\n                                     + \" %s varchar(255), \"\n                                     + \" %s varchar(25), \"\n                                     + \" %s varchar(255), \"\n                                     + \" %s varchar(255), \"\n                                     + \" %s varchar(25), \"\n                                     + \" %s varchar(255), \"\n                                     + \" %s BIGINT, \" \n                                     + \" %s BIGINT, \" \n                                     + \" %s BIGINT, \"\n\t\t\t\t\t\t\t         + \" %s BIGINT, \"\n\t\t\t\t\t                 + \" %s BIGINT, \"\n\t\t\t\t\t                 + \" %s varchar(8), \"\n                                     + \" %s varchar(4096) )\",\n                                     table,\n                                     AWSResource.FIELD_RESOURCE_ID,\n                                     AWSResource.FIELD_RESOURCE_TYPE,\n                                     AWSResource.FIELD_REGION,\n                                     AWSResource.FIELD_OWNER_EMAIL,\n                             \t\t AWSResource.FIELD_DESCRIPTION,\n                                     AWSResource.FIELD_STATE,\n                                     AWSResource.FIELD_TERMINATION_REASON,\n                                     AWSResource.FIELD_EXPECTED_TERMINATION_TIME,\n                                     AWSResource.FIELD_ACTUAL_TERMINATION_TIME,\n\t\t\t\t\t                 AWSResource.FIELD_NOTIFICATION_TIME,\n                                     AWSResource.FIELD_LAUNCH_TIME,\n                                     AWSResource.FIELD_MARK_TIME,\n\t\t\t\t\t                 AWSResource.FIELD_OPT_OUT_OF_JANITOR,\n                                     \"additionalFields\");\n            LOGGER.debug(\"Create SQL is: '{}'\", sql);\n            jdbcTemplate.execute(sql);\n            \n        } catch (AmazonClientException e) {\n            LOGGER.warn(\"Error while trying to auto-create RDS table\", e);\n        }\n    }    \n    \n    private HashMap<String, String> additionalFieldsAsMap(Resource resource) {\n    \tHashMap<String, String> fields = new HashMap<>();\n    \tfor(String key : resource.getAdditionalFieldNames()) {\n    \t\tfields.put(key, resource.getAdditionalField(key));\n    \t}\n\t\treturn fields;\n\t}    \n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/SimpleDBJanitorResourceTracker.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor;\n\nimport com.amazonaws.services.simpledb.AmazonSimpleDB;\nimport com.amazonaws.services.simpledb.model.*;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.Resource.CleanupState;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.JanitorResourceTracker;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The JanitorResourceTracker implementation in SimpleDB.\n */\npublic class SimpleDBJanitorResourceTracker implements JanitorResourceTracker {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDBJanitorResourceTracker.class);\n\n    /** The domain. */\n    private final String domain;\n\n    /** The SimpleDB client. */\n    private final AmazonSimpleDB simpleDBClient;\n\n    /**\n     * Instantiates a new simple db resource tracker.\n     *\n     * @param awsClient\n     *            the AWS Client\n     * @param domain\n     *            the domain\n     */\n    public SimpleDBJanitorResourceTracker(AWSClient awsClient, String domain) {\n        this.domain = domain;\n        this.simpleDBClient = awsClient.sdbClient();\n    }\n\n    /**\n     * Gets the SimpleDB client.\n     * @return the SimpleDB client\n     */\n    protected AmazonSimpleDB getSimpleDBClient() {\n        return simpleDBClient;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void addOrUpdate(Resource resource) {\n        List<ReplaceableAttribute> attrs = new ArrayList<ReplaceableAttribute>();\n        Map<String, String> fieldToValueMap = resource.getFieldToValueMap();\n        for (Map.Entry<String, String> entry : fieldToValueMap.entrySet()) {\n            attrs.add(new ReplaceableAttribute(entry.getKey(), entry.getValue(), true));\n        }\n        PutAttributesRequest putReqest = new PutAttributesRequest(domain, getSimpleDBItemName(resource), attrs);\n        LOGGER.debug(String.format(\"Saving resource %s to SimpleDB domain %s\",\n                resource.getId(), domain));\n        this.simpleDBClient.putAttributes(putReqest);\n        LOGGER.debug(\"Successfully saved.\");\n    }\n\n    /**\n     * Returns a list of AWSResource objects. You need to override this method if more\n     * specific resource types (e.g. subtypes of AWSResource) need to be obtained from\n     * the SimpleDB.\n     */\n    @Override\n    public List<Resource> getResources(ResourceType resourceType, CleanupState state, String resourceRegion) {\n        Validate.notEmpty(resourceRegion);\n        List<Resource> resources = new ArrayList<Resource>();\n        StringBuilder query = new StringBuilder();\n        query.append(String.format(\"select * from `%s` where \", domain));\n        if (resourceType != null) {\n            query.append(String.format(\"resourceType='%s' and \", resourceType));\n        }\n        if (state != null) {\n            query.append(String.format(\"state='%s' and \", state));\n        }\n        query.append(String.format(\"region='%s'\", resourceRegion));\n\n        LOGGER.debug(String.format(\"Query is '%s'\", query));\n\n        List<Item> items = querySimpleDBItems(query.toString());\n        for (Item item : items) {\n            try {\n                resources.add(parseResource(item));\n            } catch (Exception e) {\n                // Ignore the item that cannot be parsed.\n                LOGGER.error(String.format(\"SimpleDB item %s cannot be parsed into a resource.\", item));\n            }\n        }\n        LOGGER.info(String.format(\"Retrieved %d resources from SimpleDB in domain %s for resource type %s\"\n                + \" and state %s and region %s\",\n                resources.size(), domain, resourceType, state, resourceRegion));\n        return resources;\n    }\n\n    @Override\n    public Resource getResource(String resourceId) {\n        Validate.notEmpty(resourceId);\n        StringBuilder query = new StringBuilder();\n        query.append(String.format(\"select * from `%s` where resourceId = '%s'\", domain, resourceId));\n\n        LOGGER.debug(String.format(\"Query is '%s'\", query));\n\n        List<Item> items = querySimpleDBItems(query.toString());\n        Validate.isTrue(items.size() <= 1);\n        if (items.size() == 0) {\n            LOGGER.info(String.format(\"Not found resource with id %s\", resourceId));\n            return null;\n        } else {\n            Resource resource = null;\n            try {\n                resource = parseResource(items.get(0));\n            } catch (Exception e) {\n                // Ignore the item that cannot be parsed.\n                LOGGER.error(String.format(\"SimpleDB item %s cannot be parsed into a resource.\", items.get(0)));\n            }\n            return resource;\n        }\n    }\n\n    @Override\n    public Resource getResource(String resourceId, String region) {\n        Validate.notEmpty(resourceId);\n        Validate.notEmpty(region);\n        StringBuilder query = new StringBuilder();\n        query.append(String.format(\"select * from `%s` where resourceId = '%s' and region = '%s'\", domain, resourceId, region));\n\n        LOGGER.debug(String.format(\"Query is '%s'\", query));\n\n        List<Item> items = querySimpleDBItems(query.toString());\n        Validate.isTrue(items.size() <= 1);\n        if (items.size() == 0) {\n            LOGGER.info(String.format(\"Not found resource with id %s and region %s\", resourceId, region));\n            return null;\n        } else {\n            Resource resource = null;\n            try {\n                resource = parseResource(items.get(0));\n            } catch (Exception e) {\n                // Ignore the item that cannot be parsed.\n                LOGGER.error(String.format(\"SimpleDB item %s cannot be parsed into a resource.\", items.get(0)));\n            }\n            return resource;\n        }\n    }\n\n    /**\n     * Parses a SimpleDB item into an AWS resource.\n     * @param item the item from SimpleDB\n     * @return the AWSResource object for the SimpleDB item\n     */\n    protected Resource parseResource(Item item) {\n        Map<String, String> fieldToValue = new HashMap<String, String>();\n        for (Attribute attr : item.getAttributes()) {\n            String name = attr.getName();\n            String value = attr.getValue();\n            if (name != null && value != null) {\n                fieldToValue.put(name, value);\n            }\n        }\n        return AWSResource.parseFieldtoValueMap(fieldToValue);\n    }\n\n    /**\n     * Gets the unique SimpleDB item name for a resource. The subclass can override this\n     * method to generate the item name differently.\n     * @param resource\n     * @return the SimpleDB item name for the resource\n     */\n    protected String getSimpleDBItemName(Resource resource) {\n        return String.format(\"%s-%s-%s\", resource.getResourceType().name(), resource.getId(), resource.getRegion());\n    }\n\n    private List<Item> querySimpleDBItems(String query) {\n        Validate.notNull(query);\n        String nextToken = null;\n        List<Item> items = new ArrayList<Item>();\n        do {\n            SelectRequest request = new SelectRequest(query);\n            request.setNextToken(nextToken);\n            request.setConsistentRead(Boolean.TRUE);\n            SelectResult result = this.simpleDBClient.select(request);\n            items.addAll(result.getItems());\n            nextToken = result.getNextToken();\n        } while (nextToken != null);\n\n        return items;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/VolumeTaggingMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor;\n\nimport java.util.Collection;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.google.common.collect.Maps;\n\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.services.ec2.model.Instance;\nimport com.amazonaws.services.ec2.model.Tag;\nimport com.amazonaws.services.ec2.model.Volume;\nimport com.amazonaws.services.ec2.model.VolumeAttachment;\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.MonkeyType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\n\n/**\n * A companion monkey of Janitor Monkey for tagging EBS volumes with the last attachment information.\n * In many scenarios, EBS volumes generated by applications remain unattached to instances. Amazon\n * does not keep track of last unattached time, which makes it difficult to determine its usage.\n * To solve this, this monkey will tag all EBS volumes with last owner and instance to which they are attached\n * and the time they got detached from instance. The monkey will poll and monitor EBS volumes hourly (by default).\n *\n */\npublic class VolumeTaggingMonkey extends Monkey {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(VolumeTaggingMonkey.class);\n\n    /**\n     * The Interface Context.\n     */\n    public interface Context extends Monkey.Context {\n        /**\n         * Configuration.\n         *\n         * @return the monkey configuration\n         */\n        @Override\n        MonkeyConfiguration configuration();\n\n        /**\n         * AWS clients. Using a collection of clients for supporting running one monkey for multiple regions.\n         *\n         * @return the collection of AWS clients\n         */\n        Collection<AWSClient> awsClients();\n    }\n\n    private final MonkeyConfiguration config;\n    private final Collection<AWSClient> awsClients;\n    private final MonkeyCalendar calendar;\n\n    /** We cache the global map from instance id to its owner when starting the monkey. */\n    private final Map<AWSClient, Map<String, String>> awsClientToInstanceToOwner;\n\n    /**\n     * The constructor.\n     * @param ctx the context\n     */\n    public VolumeTaggingMonkey(Context ctx) {\n        super(ctx);\n        this.config = ctx.configuration();\n        this.awsClients = ctx.awsClients();\n        this.calendar = ctx.calendar();\n        awsClientToInstanceToOwner = Maps.newHashMap();\n        for (AWSClient awsClient : awsClients) {\n            Map<String, String> instanceToOwner = Maps.newHashMap();\n            awsClientToInstanceToOwner.put(awsClient, instanceToOwner);\n            for (Instance instance : awsClient.describeInstances()) {\n                for (Tag tag : instance.getTags()) {\n                    if (tag.getKey().equals(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY)) {\n                        instanceToOwner.put(instance.getInstanceId(), tag.getValue());\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * The monkey Type.\n     */\n    public enum Type implements MonkeyType {\n        /** Volume tagging monkey. */\n        VOLUME_TAGGING\n    }\n\n    /**\n     * The event types that this monkey causes.\n     */\n    public enum EventTypes implements EventType {\n        /** The event type for tagging the volume with Janitor meta data information. */\n        TAGGING_JANITOR\n    }\n\n    @Override\n    public Type type() {\n        return Type.VOLUME_TAGGING;\n    }\n\n    @Override\n    public void doMonkeyBusiness() {\n        String prop = \"simianarmy.volumeTagging.enabled\";\n        if (config.getBoolOrElse(prop, false)) {\n            for (AWSClient awsClient : awsClients) {\n                tagVolumesWithLatestAttachment(awsClient);\n            }\n        } else {\n            LOGGER.info(String.format(\"Volume tagging monkey is not enabled. You can set %s to true to enable it.\",\n                    prop));\n        }\n    }\n\n    private void tagVolumesWithLatestAttachment(AWSClient awsClient) {\n        List<Volume> volumes = awsClient.describeVolumes();\n        LOGGER.info(String.format(\"Trying to tag %d volumes for Janitor Monkey meta data.\",\n                volumes.size()));\n        Date now = calendar.now().getTime();\n        for (Volume volume : volumes) {\n            String owner = null, instanceId = null;\n            Date lastDetachTime = null;\n            List<VolumeAttachment> attachments = volume.getAttachments();\n            List<Tag> tags = volume.getTags();\n\n            // The volume can have a special tag is it does not want to be changed/tagged\n            // by Janitor monkey.\n            if (\"donotmark\".equals(getTagValue(JanitorMonkey.JANITOR_TAG, tags))) {\n                LOGGER.info(String.format(\"The volume %s is tagged as not handled by Janitor\",\n                        volume.getVolumeId()));\n                continue;\n            }\n\n            Map<String, String> janitorMetadata = parseJanitorTag(tags);\n            // finding the instance attached most recently.\n            VolumeAttachment latest = null;\n            for (VolumeAttachment attachment : attachments) {\n                if (latest == null || latest.getAttachTime().before(attachment.getAttachTime())) {\n                    latest = attachment;\n                }\n            }\n            if (latest != null) {\n                instanceId = latest.getInstanceId();\n                owner = getOwnerEmail(instanceId, janitorMetadata, tags, awsClient);\n            }\n\n            if (latest == null || \"detached\".equals(latest.getState())) {\n                if (janitorMetadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY) == null) {\n                    // There is no attached instance and the last detached time is not set.\n                    // Use the current time as the last detached time.\n                    LOGGER.info(String.format(\"Setting the last detached time to %s for volume %s\",\n                            now, volume.getVolumeId()));\n                    lastDetachTime = now;\n                } else {\n                    LOGGER.debug(String.format(\"The volume %s was already marked as detached at time %s\",\n                            volume.getVolumeId(), janitorMetadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY)));\n                }\n            } else {\n                // The volume is currently attached to an instance\n                lastDetachTime = null;\n            }\n            String existingOwner = janitorMetadata.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n            if (owner == null && existingOwner != null) {\n                // Save the current owner in the tag when we are not able to find a owner.\n                owner = existingOwner;\n            }\n            if (needsUpdate(janitorMetadata, owner, instanceId, lastDetachTime)) {\n                Event evt = updateJanitorMetaTag(volume, instanceId, owner, lastDetachTime, awsClient);\n                if (evt != null) {\n                    context().recorder().recordEvent(evt);\n                }\n            }\n        }\n    }\n\n    private String getOwnerEmail(String instanceId, Map<String, String> janitorMetadata,\n                                 List<Tag> tags, AWSClient awsClient) {\n        // The owner of the volume is set as the owner of the last instance attached to it.\n        String owner = awsClientToInstanceToOwner.get(awsClient).get(instanceId);\n        if (owner == null) {\n            owner = janitorMetadata.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n        }\n        if (owner == null) {\n            owner = getTagValue(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY, tags);\n        }\n        String emailDomain = getOwnerEmailDomain();\n        if (owner != null && !owner.contains(\"@\")\n                && StringUtils.isNotBlank(emailDomain)) {\n            owner = String.format(\"%s@%s\", owner, emailDomain);\n        }\n        return owner;\n    }\n\n    /**\n     * Parses the Janitor meta tag set by this monkey and gets a map from key\n     * to value for the tag values.\n     * @param tags the tags of the volumes\n     * @return the map from the Janitor meta tag key to value\n     */\n    private static Map<String, String> parseJanitorTag(List<Tag> tags) {\n        String janitorTag = getTagValue(JanitorMonkey.JANITOR_META_TAG, tags);\n        return parseJanitorMetaTag(janitorTag);\n    }\n\n    /**\n     * Parses the string of Janitor meta-data tag value to get a key value map.\n     * @param janitorMetaTag the value of the Janitor meta-data tag\n     * @return the key value map in the Janitor meta-data tag\n     */\n    public static Map<String, String> parseJanitorMetaTag(String janitorMetaTag) {\n        Map<String, String> metadata = new HashMap<String, String>();\n        if (janitorMetaTag != null) {\n            for (String keyValue : janitorMetaTag.split(\";\")) {\n                String[] meta = keyValue.split(\"=\");\n                if (meta.length == 2) {\n                    metadata.put(meta[0], meta[1]);\n                }\n            }\n        }\n        return metadata;\n    }\n\n    /** Gets the domain name for the owner email. The method can be overridden in subclasses.\n     *\n     * @return the domain name for the owner email.\n     */\n    protected String getOwnerEmailDomain() {\n        return config.getStrOrElse(\"simianarmy.volumeTagging.ownerEmailDomain\", \"\");\n    }\n\n    private Event updateJanitorMetaTag(Volume volume, String instance, String owner, Date lastDetachTime,\n                                       AWSClient awsClient) {\n        String meta = makeMetaTag(instance, owner, lastDetachTime);\n        Map<String, String> janitorTags = new HashMap<String, String>();\n        janitorTags.put(JanitorMonkey.JANITOR_META_TAG, meta);\n        LOGGER.info(String.format(\"Setting tag %s to '%s' for volume %s\",\n                JanitorMonkey.JANITOR_META_TAG, meta, volume.getVolumeId()));\n        String prop = \"simianarmy.volumeTagging.leashed\";\n        Event evt = null;\n        if (config.getBoolOrElse(prop, true)) {\n            LOGGER.info(\"Volume tagging monkey is leashed. No real change is made to the volume.\");\n        } else {\n            try {\n                awsClient.createTagsForResources(janitorTags, volume.getVolumeId());\n                evt = context().recorder().newEvent(type(), EventTypes.TAGGING_JANITOR,\n                        awsClient.region(), volume.getVolumeId());\n                evt.addField(JanitorMonkey.JANITOR_META_TAG, meta);\n            } catch (Exception e) {\n                LOGGER.error(String.format(\"Failed to update the tag for volume %s\", volume.getVolumeId()));\n            }\n        }\n        return evt;\n    }\n\n    /**\n     * Makes the Janitor meta tag for volumes to track the last attachment/detachment information.\n     * The method is intentionally made public for testing.\n     * @param instance the last attached instance\n     * @param owner the last owner\n     * @param lastDetachTime the detach time\n     * @return the meta tag of Janitor Monkey\n     */\n    public static String makeMetaTag(String instance, String owner, Date lastDetachTime) {\n        StringBuilder meta = new StringBuilder();\n        meta.append(String.format(\"%s=%s;\",\n                JanitorMonkey.INSTANCE_TAG_KEY, instance == null ? \"\" : instance));\n        meta.append(String.format(\"%s=%s;\", BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY, owner == null ? \"\" : owner));\n        meta.append(String.format(\"%s=%s\", JanitorMonkey.DETACH_TIME_TAG_KEY,\n                lastDetachTime == null ? \"\" : AWSResource.DATE_FORMATTER.print(lastDetachTime.getTime())));\n        return meta.toString();\n    }\n\n    private static String getTagValue(String key, List<Tag> tags) {\n        for (Tag tag : tags) {\n            if (tag.getKey().equals(key)) {\n                return tag.getValue();\n            }\n        }\n        return null;\n    }\n\n    /** Needs to update tags of the volume if\n     * 1) owner or instance attached changed or\n     * 2) the last detached status is changed.\n     */\n    private static boolean needsUpdate(Map<String, String> metadata,\n            String owner, String instance, Date lastDetachTime) {\n        return (owner != null && !StringUtils.equals(metadata.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY), owner))\n                || (instance != null && !StringUtils.equals(metadata.get(JanitorMonkey.INSTANCE_TAG_KEY), instance))\n                || lastDetachTime != null;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/ASGJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.Instance;\nimport com.amazonaws.services.autoscaling.model.LaunchConfiguration;\nimport com.amazonaws.services.autoscaling.model.SuspendedProcess;\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\n/**\n * The crawler to crawl AWS auto scaling groups for janitor monkey.\n */\npublic class ASGJanitorCrawler extends AbstractAWSJanitorCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ASGJanitorCrawler.class);\n\n    /** The name representing the additional field name of instance ids. */\n    public static final String ASG_FIELD_INSTANCES = \"INSTANCES\";\n\n    /** The name representing the additional field name of max ASG size. */\n    public static final String ASG_FIELD_MAX_SIZE = \"MAX_SIZE\";\n\n    /** The name representing the additional field name of ELB names. */\n    public static final String ASG_FIELD_ELBS = \"ELBS\";\n\n    /** The name representing the additional field name of launch configuration name. */\n    public static final String ASG_FIELD_LC_NAME = \"LAUNCH_CONFIGURATION_NAME\";\n\n    /** The name representing the additional field name of launch configuration creation time. */\n    public static final String ASG_FIELD_LC_CREATION_TIME = \"LAUNCH_CONFIGURATION_CREATION_TIME\";\n\n    /** The name representing the additional field name of ASG suspension time from ELB. */\n    public static final String ASG_FIELD_SUSPENSION_TIME = \"ASG_SUSPENSION_TIME\";\n\n    private final Map<String, LaunchConfiguration> nameToLaunchConfig = new HashMap<String, LaunchConfiguration>();\n\n    /** The regular expression patter below is for the termination reason added by AWS when\n     * an ASG is suspended from ELB's traffic.\n     */\n    private static final Pattern SUSPENSION_REASON_PATTERN =\n            Pattern.compile(\"User suspended at (\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}).*\");\n\n    /** The date format used to print or parse the suspension time value. **/\n    public static final DateTimeFormatter SUSPENSION_TIME_FORMATTER =\n            DateTimeFormat.forPattern(\"yyyy-MM-dd'T'HH:mm:ss\");\n\n    /**\n     * Instantiates a new basic ASG crawler.\n     * @param awsClient\n     *            the aws client\n     */\n    public ASGJanitorCrawler(AWSClient awsClient) {\n        super(awsClient);\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.ASG);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"ASG\".equals(resourceType.name())) {\n            return getASGResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... asgNames) {\n        return getASGResources(asgNames);\n    }\n\n    private List<Resource> getASGResources(String... asgNames) {\n        AWSClient awsClient = getAWSClient();\n\n        List<LaunchConfiguration> launchConfigurations = awsClient.describeLaunchConfigurations();\n        for (LaunchConfiguration lc : launchConfigurations) {\n            nameToLaunchConfig.put(lc.getLaunchConfigurationName(), lc);\n        }\n\n        List<Resource> resources = new LinkedList<Resource>();\n        for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups(asgNames)) {\n            Resource asgResource = new AWSResource().withId(asg.getAutoScalingGroupName())\n                    .withResourceType(AWSResourceType.ASG).withRegion(awsClient.region())\n                    .withLaunchTime(asg.getCreatedTime());\n            for (TagDescription tag : asg.getTags()) {\n                asgResource.setTag(tag.getKey(), tag.getValue());\n            }\n            asgResource.setDescription(String.format(\"%d instances\", asg.getInstances().size()));\n            asgResource.setOwnerEmail(getOwnerEmailForResource(asgResource));\n            if (asg.getStatus() != null) {\n                ((AWSResource) asgResource).setAWSResourceState(asg.getStatus());\n            }\n            Integer maxSize = asg.getMaxSize();\n            if (maxSize != null) {\n                asgResource.setAdditionalField(ASG_FIELD_MAX_SIZE, String.valueOf(maxSize));\n            }\n            // Adds instances and ELBs as additional fields.\n            List<String> instances = new ArrayList<String>();\n            for (Instance instance : asg.getInstances()) {\n                instances.add(instance.getInstanceId());\n            }\n            asgResource.setAdditionalField(ASG_FIELD_INSTANCES, StringUtils.join(instances, \",\"));\n            asgResource.setAdditionalField(ASG_FIELD_ELBS,\n                    StringUtils.join(asg.getLoadBalancerNames(), \",\"));\n            String lcName = asg.getLaunchConfigurationName();\n            LaunchConfiguration lc = nameToLaunchConfig.get(lcName);\n            if (lc != null) {\n                asgResource.setAdditionalField(ASG_FIELD_LC_NAME, lcName);\n            }\n            if (lc != null && lc.getCreatedTime() != null) {\n                asgResource.setAdditionalField(ASG_FIELD_LC_CREATION_TIME,\n                        String.valueOf(lc.getCreatedTime().getTime()));\n            }\n            // sets the field for the time when the ASG's traffic is suspended from ELB\n            for (SuspendedProcess sp : asg.getSuspendedProcesses()) {\n                if (\"AddToLoadBalancer\".equals(sp.getProcessName())) {\n                    String suspensionTime = getSuspensionTimeString(sp.getSuspensionReason());\n                    if (suspensionTime != null) {\n                        LOGGER.info(String.format(\"Suspension time of ASG %s is %s\",\n                                asg.getAutoScalingGroupName(), suspensionTime));\n                        asgResource.setAdditionalField(ASG_FIELD_SUSPENSION_TIME, suspensionTime);\n                        break;\n                    }\n                }\n            }\n            resources.add(asgResource);\n        }\n        return resources;\n    }\n\n    private String getSuspensionTimeString(String suspensionReason) {\n        if (suspensionReason == null) {\n            return null;\n        }\n        Matcher matcher = SUSPENSION_REASON_PATTERN.matcher(suspensionReason);\n        if (matcher.matches()) {\n            return matcher.group(1);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/AbstractAWSJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport org.apache.commons.lang.Validate;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.JanitorCrawler;\n\n/**\n * The abstract class for crawler of AWS resources.\n */\npublic abstract class AbstractAWSJanitorCrawler implements JanitorCrawler {\n    /** The AWS client. */\n    private final AWSClient awsClient;\n\n    /**\n     * The constructor.\n     * @param awsClient the AWS client used by the crawler.\n     */\n    public AbstractAWSJanitorCrawler(AWSClient awsClient) {\n        Validate.notNull(awsClient);\n        this.awsClient = awsClient;\n    }\n\n    /**\n     * Gets the owner email from the resource's tag key set in GLOBAL_OWNER_TAGKEY.\n     * @param resource the resource\n     * @return the owner email specified in the resource's tags\n     */\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        Validate.notNull(resource);\n        return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n    }\n\n    /**\n     * Gets the AWS client used by the crawler.\n     * @return the AWS client used by the crawler.\n     */\n    protected AWSClient getAWSClient() {\n        return awsClient;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSSnapshotJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.services.ec2.model.BlockDeviceMapping;\nimport com.amazonaws.services.ec2.model.EbsBlockDevice;\nimport com.amazonaws.services.ec2.model.Image;\nimport com.amazonaws.services.ec2.model.Snapshot;\nimport com.amazonaws.services.ec2.model.Tag;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\n/**\n * The crawler to crawl AWS EBS snapshots for janitor monkey.\n */\npublic class EBSSnapshotJanitorCrawler extends AbstractAWSJanitorCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EBSSnapshotJanitorCrawler.class);\n\n    /** The name representing the additional field name of AMIs generated using the snapshot. */\n    public static final String SNAPSHOT_FIELD_AMIS = \"AMIs\";\n\n\n    /** The map from snapshot id to the AMI ids that are generated using the snapshot. */\n    private final Map<String, Collection<String>> snapshotToAMIs =\n            new HashMap<String, Collection<String>>();\n\n    /**\n     * The constructor.\n     * @param awsClient the AWS client\n     */\n    public EBSSnapshotJanitorCrawler(AWSClient awsClient) {\n        super(awsClient);\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.EBS_SNAPSHOT);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"EBS_SNAPSHOT\".equals(resourceType.name())) {\n            return getSnapshotResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getSnapshotResources(resourceIds);\n    }\n\n    private List<Resource> getSnapshotResources(String... snapshotIds) {\n        refreshSnapshotToAMIs();\n\n        List<Resource> resources = new LinkedList<Resource>();\n        AWSClient awsClient = getAWSClient();\n\n        for (Snapshot snapshot : awsClient.describeSnapshots(snapshotIds)) {\n            Resource snapshotResource = new AWSResource().withId(snapshot.getSnapshotId())\n                    .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                    .withLaunchTime(snapshot.getStartTime()).withDescription(snapshot.getDescription());\n            for (Tag tag : snapshot.getTags()) {\n                LOGGER.debug(String.format(\"Adding tag %s = %s to resource %s\",\n                        tag.getKey(), tag.getValue(), snapshotResource.getId()));\n                snapshotResource.setTag(tag.getKey(), tag.getValue());\n            }\n            snapshotResource.setOwnerEmail(getOwnerEmailForResource(snapshotResource));\n            ((AWSResource) snapshotResource).setAWSResourceState(snapshot.getState());\n            Collection<String> amis = snapshotToAMIs.get(snapshotResource.getId());\n            if (amis != null) {\n                snapshotResource.setAdditionalField(SNAPSHOT_FIELD_AMIS, StringUtils.join(amis, \",\"));\n            }\n            resources.add(snapshotResource);\n        }\n        return resources;\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        Validate.notNull(resource);\n        String owner = resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n        if (owner == null) {\n            owner = super.getOwnerEmailForResource(resource);\n        }\n        return owner;\n    }\n\n    /**\n     * Gets the collection of AMIs that are created using a specific snapshot.\n     * @param snapshotId the snapshot id\n     */\n    protected Collection<String> getAMIsForSnapshot(String snapshotId) {\n        Collection<String> amis = snapshotToAMIs.get(snapshotId);\n        if (amis != null) {\n            return Collections.unmodifiableCollection(amis);\n        } else {\n            return Collections.emptyList();\n        }\n    }\n\n    private void refreshSnapshotToAMIs() {\n        snapshotToAMIs.clear();\n        for (Image image : getAWSClient().describeImages()) {\n            for (BlockDeviceMapping bdm : image.getBlockDeviceMappings()) {\n                EbsBlockDevice ebd = bdm.getEbs();\n                if (ebd != null && ebd.getSnapshotId() != null) {\n                    LOGGER.debug(String.format(\"Snapshot %s is used to generate AMI %s\",\n                            ebd.getSnapshotId(), image.getImageId()));\n                    Collection<String> amis = snapshotToAMIs.get(ebd.getSnapshotId());\n                    if (amis == null) {\n                        amis = new ArrayList<String>();\n                        snapshotToAMIs.put(ebd.getSnapshotId(), amis);\n                    }\n                    amis.add(image.getImageId());\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSVolumeJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.services.ec2.model.Tag;\nimport com.amazonaws.services.ec2.model.Volume;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\n\n/**\n * The crawler to crawl AWS EBS volumes for janitor monkey.\n */\npublic class EBSVolumeJanitorCrawler extends AbstractAWSJanitorCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EBSVolumeJanitorCrawler.class);\n\n    /**\n     * The constructor.\n     * @param awsClient the AWS client\n     */\n    public EBSVolumeJanitorCrawler(AWSClient awsClient) {\n        super(awsClient);\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.EBS_VOLUME);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"EBS_VOLUME\".equals(resourceType.name())) {\n            return getVolumeResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getVolumeResources(resourceIds);\n    }\n\n    private List<Resource> getVolumeResources(String... volumeIds) {\n        List<Resource> resources = new LinkedList<Resource>();\n\n        AWSClient awsClient = getAWSClient();\n\n        for (Volume volume : awsClient.describeVolumes(volumeIds)) {\n            Resource volumeResource = new AWSResource().withId(volume.getVolumeId())\n                    .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.EBS_VOLUME)\n                    .withLaunchTime(volume.getCreateTime());\n            for (Tag tag : volume.getTags()) {\n                LOGGER.info(String.format(\"Adding tag %s = %s to resource %s\",\n                        tag.getKey(), tag.getValue(), volumeResource.getId()));\n                volumeResource.setTag(tag.getKey(), tag.getValue());\n            }\n            volumeResource.setOwnerEmail(getOwnerEmailForResource(volumeResource));\n            volumeResource.setDescription(getVolumeDescription(volume));\n            ((AWSResource) volumeResource).setAWSResourceState(volume.getState());\n            resources.add(volumeResource);\n        }\n        return resources;\n    }\n\n    private String getVolumeDescription(Volume volume) {\n        StringBuilder description = new StringBuilder();\n        Integer size = volume.getSize();\n        description.append(String.format(\"size=%s\", size == null ? \"unknown\" : size));\n        for (Tag tag : volume.getTags()) {\n            description.append(String.format(\"; %s=%s\", tag.getKey(), tag.getValue()));\n        }\n        return description.toString();\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        String owner = super.getOwnerEmailForResource(resource);\n        if (owner == null) {\n            // try to find the owner from Janitor Metadata tag set by the volume tagging monkey.\n            Map<String, String> janitorTag = VolumeTaggingMonkey.parseJanitorMetaTag(resource.getTag(\n                    JanitorMonkey.JANITOR_META_TAG));\n            owner = janitorTag.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n        }\n        return owner;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/ELBJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.elasticloadbalancing.model.Instance;\nimport com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription;\nimport com.amazonaws.services.elasticloadbalancing.model.Tag;\nimport com.amazonaws.services.elasticloadbalancing.model.TagDescription;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.*;\n\n/**\n * The crawler to crawl AWS instances for janitor monkey.\n */\npublic class ELBJanitorCrawler extends AbstractAWSJanitorCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ELBJanitorCrawler.class);\n\n    /**\n     * Instantiates a new basic instance crawler.\n     * @param awsClient\n     *            the aws client\n     */\n    public ELBJanitorCrawler(AWSClient awsClient) {\n        super(awsClient);\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.ELB);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"ELB\".equals(resourceType.name())) {\n            return getELBResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getELBResources(resourceIds);\n    }\n\n    private List<Resource> getELBResources(String... elbNames) {\n        List<Resource> resources = new LinkedList<Resource>();\n        AWSClient awsClient = getAWSClient();\n\n        for (LoadBalancerDescription elb : awsClient.describeElasticLoadBalancers(elbNames)) {\n            Resource resource = new AWSResource().withId(elb.getLoadBalancerName())\n                    .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.ELB)\n                    .withLaunchTime(elb.getCreatedTime());\n            resource.setOwnerEmail(getOwnerEmailForResource(resource));\n            resources.add(resource);\n            List<Instance> instances = elb.getInstances();\n            if (instances == null || instances.size() == 0) {\n                resource.setAdditionalField(\"instanceCount\", \"0\");\n                resource.setDescription(\"instances=none\");\n                LOGGER.debug(String.format(\"No instances found for ELB %s\", resource.getId()));\n            } else {\n                resource.setAdditionalField(\"instanceCount\", \"\" + instances.size());\n                ArrayList<String> instanceList = new ArrayList<String>(instances.size());\n                LOGGER.debug(String.format(\"Found %d instances for ELB %s\", instances.size(), resource.getId()));\n                for (Instance instance : instances) {\n                    String instanceId = instance.getInstanceId();\n                    instanceList.add(instanceId);\n                }\n                String instancesStr = StringUtils.join(instanceList,\",\");\n                resource.setDescription(String.format(\"instances=%s\", instances));\n                LOGGER.debug(String.format(\"Resource ELB %s has instances %s\", resource.getId(), instancesStr));\n            }\n\n            for(TagDescription tagDescription : awsClient.describeElasticLoadBalancerTags(resource.getId())) {\n                for(Tag tag : tagDescription.getTags()) {\n                    LOGGER.debug(String.format(\"Adding tag %s = %s to resource %s\",\n                            tag.getKey(), tag.getValue(), resource.getId()));\n                    resource.setTag(tag.getKey(), tag.getValue());\n                }\n            }\n        }\n\n        Map<String, List<String>> elbtoASGMap = buildELBtoASGMap();\n        for(Resource resource : resources) {\n            List<String> asgList = elbtoASGMap.get(resource.getId());\n            if (asgList != null && asgList.size() > 0) {\n                resource.setAdditionalField(\"referencedASGCount\", \"\" + asgList.size());\n                String asgStr = StringUtils.join(asgList,\",\");\n                resource.setDescription(resource.getDescription() + \", ASGS=\" + asgStr);\n                LOGGER.debug(String.format(\"Resource ELB %s is referenced by ASGs %s\", resource.getId(), asgStr));\n            } else {\n                resource.setAdditionalField(\"referencedASGCount\", \"0\");\n                resource.setDescription(resource.getDescription() + \", ASGS=none\");\n                LOGGER.debug(String.format(\"No ASGs found for ELB %s\", resource.getId()));\n            }\n        }\n\n        return resources;\n    }\n\n    private Map<String, List<String>> buildELBtoASGMap() {\n        AWSClient awsClient = getAWSClient();\n        LOGGER.info(String.format(\"Getting all ELBs associated with ASGs in region %s\", awsClient.region()));\n\n        List<AutoScalingGroup> autoScalingGroupList = awsClient.describeAutoScalingGroups();\n        HashMap<String, List<String>> asgMap = new HashMap<>();\n        for (AutoScalingGroup asg : autoScalingGroupList) {\n            String asgName = asg.getAutoScalingGroupName();\n            if (asg.getLoadBalancerNames() != null ) {\n                for (String elbName : asg.getLoadBalancerNames()) {\n                    List<String> asgList = asgMap.get(elbName);\n                    if (asgList == null) {\n                        asgList = new ArrayList<>();\n                        asgMap.put(elbName, asgList);\n                    }\n                    asgList.add(asgName);\n                    LOGGER.debug(String.format(\"Found ASG %s associated with ELB %s\", asgName, elbName));\n                }\n            }\n        }\n        return asgMap;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/InstanceJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingInstanceDetails;\nimport com.amazonaws.services.ec2.model.Instance;\nimport com.amazonaws.services.ec2.model.Tag;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\n/**\n * The crawler to crawl AWS instances for janitor monkey.\n */\npublic class InstanceJanitorCrawler extends AbstractAWSJanitorCrawler {\n\n    /** The name representing the additional field name of ASG's name. */\n    public static final String INSTANCE_FIELD_ASG_NAME = \"ASG_NAME\";\n\n    /** The name representing the additional field name of the OpsWork stack name. */\n    public static final String  INSTANCE_FIELD_OPSWORKS_STACK_NAME = \"OPSWORKS_STACK_NAME\";\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceJanitorCrawler.class);\n\n    /**\n     * Instantiates a new basic instance crawler.\n     * @param awsClient\n     *            the aws client\n     */\n    public InstanceJanitorCrawler(AWSClient awsClient) {\n        super(awsClient);\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.INSTANCE);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"INSTANCE\".equals(resourceType.name())) {\n            return getInstanceResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getInstanceResources(resourceIds);\n    }\n\n    private List<Resource> getInstanceResources(String... instanceIds) {\n        List<Resource> resources = new LinkedList<Resource>();\n\n        AWSClient awsClient = getAWSClient();\n        Map<String, AutoScalingInstanceDetails> idToASGInstance = new HashMap<String, AutoScalingInstanceDetails>();\n        for (AutoScalingInstanceDetails instanceDetails : awsClient.describeAutoScalingInstances(instanceIds)) {\n            idToASGInstance.put(instanceDetails.getInstanceId(), instanceDetails);\n        }\n\n        for (Instance instance : awsClient.describeInstances(instanceIds)) {\n            Resource instanceResource = new AWSResource().withId(instance.getInstanceId())\n                    .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.INSTANCE)\n                    .withLaunchTime(instance.getLaunchTime());\n            for (Tag tag : instance.getTags()) {\n                instanceResource.setTag(tag.getKey(), tag.getValue());\n            }\n            String description = String.format(\"type=%s; host=%s\", instance.getInstanceType(),\n                    instance.getPublicDnsName() == null ? \"\" : instance.getPublicDnsName());\n            instanceResource.setDescription(description);\n            instanceResource.setOwnerEmail(getOwnerEmailForResource(instanceResource));\n\n            String asgName = getAsgName(instanceResource, idToASGInstance);\n            if (asgName != null) {\n                instanceResource.setAdditionalField(INSTANCE_FIELD_ASG_NAME, asgName);\n                LOGGER.info(String.format(\"instance %s has a ASG tag name %s.\", instanceResource.getId(), asgName));\n            }\n            String opsworksStackName = getOpsWorksStackName(instanceResource);\n            if (opsworksStackName != null) {\n                instanceResource.setAdditionalField(INSTANCE_FIELD_OPSWORKS_STACK_NAME, opsworksStackName);\n                LOGGER.info(String.format(\"instance %s is part of an OpsWorks stack named %s.\", instanceResource.getId(), opsworksStackName));\n            }\n            if (instance.getState() != null) {\n                ((AWSResource) instanceResource).setAWSResourceState(instance.getState().getName());\n            }\n            resources.add(instanceResource);\n        }\n        return resources;\n    }\n\n    private String getAsgName(Resource instanceResource, Map<String, AutoScalingInstanceDetails> idToASGInstance) {\n        String asgName = instanceResource.getTag(\"aws:autoscaling:groupName\");\n        if (asgName == null) {\n            // At most times the aws:autoscaling:groupName tag has the ASG name, but there are cases\n            // that the instance is not correctly tagged and we can find the ASG name from AutoScaling\n            // service.\n            AutoScalingInstanceDetails instanceDetails = idToASGInstance.get(instanceResource.getId());\n            if (instanceDetails != null) {\n                asgName = instanceDetails.getAutoScalingGroupName();\n            }\n        }\n        return asgName;\n    }\n\n    private String getOpsWorksStackName(Resource instanceResource) {\n        return instanceResource.getTag(\"opsworks:stack\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/LaunchConfigJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.LaunchConfiguration;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Sets;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * The crawler to crawl AWS launch configurations for janitor monkey.\n */\npublic class LaunchConfigJanitorCrawler extends AbstractAWSJanitorCrawler {\n\n    /** The name representing the additional field name of a flag indicating if the launch config\n     * if used by an auto scaling group. */\n    public static final String LAUNCH_CONFIG_FIELD_USED_BY_ASG = \"USED_BY_ASG\";\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(LaunchConfigJanitorCrawler.class);\n\n    /**\n     * Instantiates a new basic launch configuration crawler.\n     * @param awsClient\n     *            the aws client\n     */\n    public LaunchConfigJanitorCrawler(AWSClient awsClient) {\n        super(awsClient);\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.LAUNCH_CONFIG);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"LAUNCH_CONFIG\".equals(resourceType.name())) {\n            return getLaunchConfigResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getLaunchConfigResources(resourceIds);\n    }\n\n    private List<Resource> getLaunchConfigResources(String... launchConfigNames) {\n        List<Resource> resources = Lists.newArrayList();\n\n        AWSClient awsClient = getAWSClient();\n\n        Set<String> usedLCs = Sets.newHashSet();\n        for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups()) {\n            usedLCs.add(asg.getLaunchConfigurationName());\n        }\n\n        for (LaunchConfiguration launchConfiguration : awsClient.describeLaunchConfigurations(launchConfigNames)) {\n            String lcName = launchConfiguration.getLaunchConfigurationName();\n            Resource lcResource = new AWSResource().withId(lcName)\n                    .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.LAUNCH_CONFIG)\n                    .withLaunchTime(launchConfiguration.getCreatedTime());\n            lcResource.setOwnerEmail(getOwnerEmailForResource(lcResource));\n\n            lcResource.setAdditionalField(LAUNCH_CONFIG_FIELD_USED_BY_ASG, String.valueOf(usedLCs.contains(lcName)));\n            resources.add(lcResource);\n        }\n        return resources;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaASGJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler.edda;\n\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport com.netflix.simianarmy.janitor.JanitorCrawler;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.codehaus.jackson.JsonNode;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.EnumSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * The crawler to crawl AWS auto scaling groups for janitor monkey using Edda.\n */\npublic class EddaASGJanitorCrawler implements JanitorCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaASGJanitorCrawler.class);\n\n    /** The name representing the additional field name of instance ids. */\n    public static final String ASG_FIELD_INSTANCES = \"INSTANCES\";\n\n    /** The name representing the additional field name of max ASG size. */\n    public static final String ASG_FIELD_MAX_SIZE = \"MAX_SIZE\";\n\n    /** The name representing the additional field name of ELB names. */\n    public static final String ASG_FIELD_ELBS = \"ELBS\";\n\n    /** The name representing the additional field name of launch configuration name. */\n    public static final String ASG_FIELD_LC_NAME = \"LAUNCH_CONFIGURATION_NAME\";\n\n    /** The name representing the additional field name of launch configuration creation time. */\n    public static final String ASG_FIELD_LC_CREATION_TIME = \"LAUNCH_CONFIGURATION_CREATION_TIME\";\n\n    /** The name representing the additional field name of ASG suspension time from ELB. */\n    public static final String ASG_FIELD_SUSPENSION_TIME = \"ASG_SUSPENSION_TIME\";\n\n    /** The name representing the additional field name of ASG's last change/activity time. */\n    public static final String ASG_FIELD_LAST_CHANGE_TIME = \"ASG_LAST_CHANGE_TIME\";\n\n    /** The regular expression patter below is for the termination reason added by AWS when\n     * an ASG is suspended from ELB's traffic.\n     */\n    private static final Pattern SUSPENSION_REASON_PATTERN =\n            Pattern.compile(\"User suspended at (\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}).*\");\n\n    private final EddaClient eddaClient;\n    private final List<String> regions = Lists.newArrayList();\n    private final Map<String, Map<String, Long>> regionToAsgToLastChangeTime = Maps.newHashMap();\n\n    /**\n     * Instantiates a new basic ASG crawler.\n     * @param eddaClient\n     *            the Edda client\n     * @param regions\n     *            the regions the crawler will crawl resources for\n     */\n    public EddaASGJanitorCrawler(EddaClient eddaClient, String... regions) {\n        Validate.notNull(eddaClient);\n        this.eddaClient = eddaClient;\n        Validate.notNull(regions);\n        for (String region : regions) {\n            this.regions.add(region);\n        }\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.ASG);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"ASG\".equals(resourceType.name())) {\n            return getASGResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... asgNames) {\n        return getASGResources(asgNames);\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        Validate.notNull(resource);\n        return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n    }\n\n    private List<Resource> getASGResources(String... asgNames) {\n        refreshAsgLastChangeTime();\n        List<Resource> resources = Lists.newArrayList();\n        for (String region : regions) {\n            resources.addAll(getASGResourcesInRegion(region, asgNames));\n        }\n        return resources;\n    }\n\n    private List<Resource> getASGResourcesInRegion(String region, String... asgNames) {\n        String url = eddaClient.getBaseUrl(region) + \"/aws/autoScalingGroups;\";\n        if (asgNames != null && asgNames.length != 0) {\n            url += StringUtils.join(asgNames, ',');\n            LOGGER.info(String.format(\"Getting ASGs in region %s for %d ids\", region, asgNames.length));\n        } else {\n            LOGGER.info(String.format(\"Getting all ASGs in region %s\", region));\n        }\n        url += \";_expand:(autoScalingGroupName,createdTime,maxSize,suspendedProcesses:(processName,suspensionReason),\"\n               + \"tags:(key,value),instances:(instanceId),loadBalancerNames,launchConfigurationName)\";\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for ASGs in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        Map<String, Long> lcNameToCreationTime = getLaunchConfigCreationTimes(region);\n        List<Resource> resources = Lists.newArrayList();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            resources.add(parseJsonElementToresource(region, it.next(), lcNameToCreationTime));\n        }\n        return resources;\n    }\n\n    private Resource parseJsonElementToresource(String region, JsonNode jsonNode\n            , Map<String, Long> lcNameToCreationTime) {\n        Validate.notNull(jsonNode);\n\n        String asgName = jsonNode.get(\"autoScalingGroupName\").getTextValue();\n        long createdTime = jsonNode.get(\"createdTime\").getLongValue();\n\n        Resource resource = new AWSResource().withId(asgName).withRegion(region)\n                .withResourceType(AWSResourceType.ASG)\n                .withLaunchTime(new Date(createdTime));\n\n        JsonNode tags = jsonNode.get(\"tags\");\n        if (tags == null || !tags.isArray() || tags.size() == 0) {\n            LOGGER.debug(String.format(\"No tags is found for %s\", resource.getId()));\n        } else {\n            for (Iterator<JsonNode> it = tags.getElements(); it.hasNext();) {\n                JsonNode tag = it.next();\n                String key = tag.get(\"key\").getTextValue();\n                String value = tag.get(\"value\").getTextValue();\n                resource.setTag(key, value);\n            }\n        }\n\n        String owner = getOwnerEmailForResource(resource);\n        if (owner != null) {\n            resource.setOwnerEmail(owner);\n        }\n        JsonNode maxSize = jsonNode.get(\"maxSize\");\n        if (maxSize != null) {\n            resource.setAdditionalField(ASG_FIELD_MAX_SIZE, String.valueOf(maxSize.getIntValue()));\n        }\n        // Adds instances and ELBs as additional fields.\n        JsonNode instances = jsonNode.get(\"instances\");\n        resource.setDescription(String.format(\"%d instances\", instances.size()));\n        List<String> instanceIds = Lists.newArrayList();\n        for (Iterator<JsonNode> it = instances.getElements(); it.hasNext();) {\n            instanceIds.add(it.next().get(\"instanceId\").getTextValue());\n        }\n        resource.setAdditionalField(ASG_FIELD_INSTANCES, StringUtils.join(instanceIds, \",\"));\n        JsonNode elbs = jsonNode.get(\"loadBalancerNames\");\n        List<String> elbNames = Lists.newArrayList();\n        for (Iterator<JsonNode> it = elbs.getElements(); it.hasNext();) {\n            elbNames.add(it.next().getTextValue());\n        }\n        resource.setAdditionalField(ASG_FIELD_ELBS, StringUtils.join(elbNames, \",\"));\n\n        JsonNode lc = jsonNode.get(\"launchConfigurationName\");\n        if (lc != null) {\n            String lcName = lc.getTextValue();\n            Long lcCreationTime = lcNameToCreationTime.get(lcName);\n            if (lcName != null) {\n                resource.setAdditionalField(ASG_FIELD_LC_NAME, lcName);\n            }\n            if (lcCreationTime != null) {\n                resource.setAdditionalField(ASG_FIELD_LC_CREATION_TIME, String.valueOf(lcCreationTime));\n            }\n        }\n        // sets the field for the time when the ASG's traffic is suspended from ELB\n        JsonNode suspendedProcesses = jsonNode.get(\"suspendedProcesses\");\n        for (Iterator<JsonNode> it = suspendedProcesses.getElements(); it.hasNext();) {\n            JsonNode sp = it.next();\n            if (\"AddToLoadBalancer\".equals(sp.get(\"processName\").getTextValue())) {\n                String suspensionTime = getSuspensionTimeString(sp.get(\"suspensionReason\").getTextValue());\n                if (suspensionTime != null) {\n                    LOGGER.info(String.format(\"Suspension time of ASG %s is %s\",\n                            asgName, suspensionTime));\n                    resource.setAdditionalField(ASG_FIELD_SUSPENSION_TIME, suspensionTime);\n                    break;\n                }\n            }\n        }\n        Long lastChangeTime = regionToAsgToLastChangeTime.get(region).get(asgName);\n        if (lastChangeTime != null) {\n            resource.setAdditionalField(ASG_FIELD_LAST_CHANGE_TIME, String.valueOf(lastChangeTime));\n        }\n        return resource;\n\n    }\n\n    private Map<String, Long> getLaunchConfigCreationTimes(String region) {\n        LOGGER.info(String.format(\"Getting launch configuration creation times in region %s\", region));\n\n        String url = eddaClient.getBaseUrl(region)\n                + \"/aws/launchConfigurations;_expand:(launchConfigurationName,createdTime)\";\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for lc creation times in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        Map<String, Long> nameToCreationTime = Maps.newHashMap();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            JsonNode elem = it.next();\n            nameToCreationTime.put(elem.get(\"launchConfigurationName\").getTextValue(),\n                    elem.get(\"createdTime\").getLongValue());\n        }\n        return nameToCreationTime;\n    }\n\n    private String getSuspensionTimeString(String suspensionReason) {\n        if (suspensionReason == null) {\n            return null;\n        }\n        Matcher matcher = SUSPENSION_REASON_PATTERN.matcher(suspensionReason);\n        if (matcher.matches()) {\n            return matcher.group(1);\n        }\n        return null;\n    }\n\n    private void refreshAsgLastChangeTime() {\n        regionToAsgToLastChangeTime.clear();\n        for (String region : regions) {\n            LOGGER.info(String.format(\"Getting ASG last change time in region %s\", region));\n            Map<String, Long> asgToLastChangeTime = regionToAsgToLastChangeTime.get(region);\n            if (asgToLastChangeTime == null) {\n                asgToLastChangeTime = Maps.newHashMap();\n                regionToAsgToLastChangeTime.put(region, asgToLastChangeTime);\n            }\n            String url = eddaClient.getBaseUrl(region) + \"/aws/autoScalingGroups;\"\n                    + \";_expand;_meta:(stime,data:(autoScalingGroupName))\";\n\n            JsonNode jsonNode = null;\n            try {\n                jsonNode = eddaClient.getJsonNodeFromUrl(url);\n            } catch (Exception e) {\n                LOGGER.error(String.format(\n                        \"Failed to get Jason node from edda for ASG last change time in region %s.\", region), e);\n            }\n\n            if (jsonNode == null || !jsonNode.isArray()) {\n                throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\",\n                        url, jsonNode));\n            }\n\n            for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n                JsonNode asg = it.next();\n                String asgName = asg.get(\"data\").get(\"autoScalingGroupName\").getTextValue();\n                Long lastChangeTime = asg.get(\"stime\").asLong();\n                LOGGER.debug(String.format(\"The last change time of ASG %s is %s\", asgName,\n                        new DateTime(lastChangeTime)));\n                asgToLastChangeTime.put(asgName, lastChangeTime);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaEBSSnapshotJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.crawler.edda;\n\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport com.netflix.simianarmy.janitor.JanitorCrawler;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.codehaus.jackson.JsonNode;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.EnumSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The crawler to crawl AWS EBS snapshots for janitor monkey using Edda.\n */\npublic class EddaEBSSnapshotJanitorCrawler implements JanitorCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaEBSSnapshotJanitorCrawler.class);\n\n    /** The name representing the additional field name of AMIs generated using the snapshot. */\n    public static final String SNAPSHOT_FIELD_AMIS = \"AMIs\";\n\n\n    /** The map from snapshot id to the AMI ids that are generated using the snapshot. */\n    private final Map<String, Collection<String>> snapshotToAMIs = Maps.newHashMap();\n\n    private final EddaClient eddaClient;\n    private final List<String> regions = Lists.newArrayList();\n    private final String defaultOwnerId;\n\n    /**\n     * The constructor.\n     * @param defaultOwnerId\n     *            the default owner id that snapshots need to have for being crawled, null means no filtering is\n     *            needed\n     * @param eddaClient\n     *            the Edda client\n     * @param regions\n     *            the regions the crawler will crawl resources for\n     */\n    public EddaEBSSnapshotJanitorCrawler(String defaultOwnerId, EddaClient eddaClient, String... regions) {\n        this.defaultOwnerId = defaultOwnerId;\n        Validate.notNull(eddaClient);\n        this.eddaClient = eddaClient;\n        Validate.notNull(regions);\n        for (String region : regions) {\n            this.regions.add(region);\n        }\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.EBS_SNAPSHOT);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"EBS_SNAPSHOT\".equals(resourceType.name())) {\n            return getSnapshotResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getSnapshotResources(resourceIds);\n    }\n\n    private List<Resource> getSnapshotResources(String... snapshotIds) {\n        List<Resource> resources = Lists.newArrayList();\n        for (String region : regions) {\n            resources.addAll(getSnapshotResourcesInRegion(region, snapshotIds));\n        }\n        return resources;\n    }\n\n    private List<Resource> getSnapshotResourcesInRegion(String region, String... snapshotIds) {\n        refreshSnapshotToAMIs(region);\n\n        String url = eddaClient.getBaseUrl(region) + \"/aws/snapshots/\";\n        if (snapshotIds != null && snapshotIds.length != 0) {\n            url += StringUtils.join(snapshotIds, ',');\n            LOGGER.info(String.format(\"Getting snapshots in region %s for %d ids\", region, snapshotIds.length));\n        } else {\n            LOGGER.info(String.format(\"Getting all snapshots in region %s\", region));\n        }\n        url += \";state=completed;_expand:(snapshotId,state,description,startTime,tags,ownerId)\";\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for snapshots in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        List<Resource> resources = Lists.newArrayList();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            JsonNode elem = it.next();\n            // Filter out shared snapshots that do not have the specified owner id.\n            String ownerId = elem.get(\"ownerId\").getTextValue();\n            if (defaultOwnerId != null && !defaultOwnerId.equals(ownerId)) {\n                LOGGER.info(String.format(\"Ignoring snapshotIds %s since it does not have the specified ownerId.\",\n                        elem.get(\"snapshotId\").getTextValue()));\n            } else {\n                resources.add(parseJsonElementToSnapshotResource(region, elem));\n            }\n        }\n        return resources;\n    }\n\n    private Resource parseJsonElementToSnapshotResource(String region, JsonNode jsonNode) {\n        Validate.notNull(jsonNode);\n        long startTime = jsonNode.get(\"startTime\").asLong();\n\n        Resource resource = new AWSResource().withId(jsonNode.get(\"snapshotId\").getTextValue()).withRegion(region)\n                .withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(startTime));\n        JsonNode tags = jsonNode.get(\"tags\");\n\n        if (tags == null || !tags.isArray() || tags.size() == 0) {\n            LOGGER.debug(String.format(\"No tags is found for %s\", resource.getId()));\n        } else {\n            for (Iterator<JsonNode> it = tags.getElements(); it.hasNext();) {\n                JsonNode tag = it.next();\n                String key = tag.get(\"key\").getTextValue();\n                String value = tag.get(\"value\").getTextValue();\n                resource.setTag(key, value);\n            }\n        }\n        JsonNode description = jsonNode.get(\"description\");\n        if (description != null) {\n            resource.setDescription(description.getTextValue());\n        }\n        ((AWSResource) resource).setAWSResourceState(jsonNode.get(\"state\").getTextValue());\n        Collection<String> amis = snapshotToAMIs.get(resource.getId());\n        if (amis != null) {\n            resource.setAdditionalField(SNAPSHOT_FIELD_AMIS, StringUtils.join(amis, \",\"));\n        }\n        resource.setOwnerEmail(getOwnerEmailForResource(resource));\n        return resource;\n    }\n\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        Validate.notNull(resource);\n        return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n    }\n\n    /**\n     * Gets the collection of AMIs that are created using a specific snapshot.\n     * @param snapshotId the snapshot id\n     */\n    protected Collection<String> getAMIsForSnapshot(String snapshotId) {\n        Collection<String> amis = snapshotToAMIs.get(snapshotId);\n        if (amis != null) {\n            return Collections.unmodifiableCollection(amis);\n        } else {\n            return Collections.emptyList();\n        }\n    }\n\n    private void refreshSnapshotToAMIs(String region) {\n        snapshotToAMIs.clear();\n\n        LOGGER.info(String.format(\"Getting mapping from snapshot to AMIs in region %s\", region));\n\n        String url = eddaClient.getBaseUrl(region) + \"/aws/images/\"\n                + \";_expand:(imageId,blockDeviceMappings:(ebs:(snapshotId)))\";\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for AMI mapping in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            JsonNode elem = it.next();\n            String imageId = elem.get(\"imageId\").getTextValue();\n            JsonNode blockMappings = elem.get(\"blockDeviceMappings\");\n            if (blockMappings == null || !blockMappings.isArray() || blockMappings.size() == 0) {\n                continue;\n            }\n            for (Iterator<JsonNode> blockMappingsIt = blockMappings.getElements(); blockMappingsIt.hasNext();) {\n                JsonNode blockMappingNode = blockMappingsIt.next();\n                JsonNode ebs = blockMappingNode.get(\"ebs\");\n                if (ebs == null) {\n                    continue;\n                }\n                JsonNode snapshotIdNode = ebs.get(\"snapshotId\");\n                String snapshotId = snapshotIdNode.getTextValue();\n                LOGGER.debug(String.format(\"Snapshot %s is used to generate AMI %s\", snapshotId, imageId));\n\n                Collection<String> amis = snapshotToAMIs.get(snapshotId);\n                if (amis == null) {\n                    amis = Lists.newArrayList();\n                    snapshotToAMIs.put(snapshotId, amis);\n                }\n                amis.add(imageId);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaEBSVolumeJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.crawler.edda;\n\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.common.collect.Sets;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport com.netflix.simianarmy.janitor.JanitorCrawler;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.codehaus.jackson.JsonNode;\nimport org.joda.time.DateTime;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.util.*;\n\n/**\n * The crawler to crawl AWS EBS volumes for Janitor monkey using Edda.\n */\npublic class EddaEBSVolumeJanitorCrawler implements JanitorCrawler {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaEBSVolumeJanitorCrawler.class);\n\n    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormat.forPattern(\"yyyy-MM-dd'T'HH:mm:ss.S'Z'\");\n\n    private static final int BATCH_SIZE = 50;\n\n    // The value below specifies how many days we want to look back in Edda to find the owner of old instances.\n    // In case of Edda keeps too much history data, without a reasonable date range, the query may fail.\n    private static final int LOOKBACK_DAYS = 90;\n\n    /**\n     * The field name for purpose.\n     */\n    public static final String PURPOSE = \"purpose\";\n\n    /**\n     * The field name for deleteOnTermination.\n     */\n    public static final String DELETE_ON_TERMINATION = \"deleteOnTermination\";\n\n    /**\n     * The field name for detach time.\n     */\n    public static final String DETACH_TIME = \"detachTime\";\n\n    private final EddaClient eddaClient;\n    private final List<String> regions = Lists.newArrayList();\n    private final Map<String, String> instanceToOwner = Maps.newHashMap();\n\n    /**\n     * The constructor.\n     * @param eddaClient\n     *            the Edda client\n     * @param regions\n     *            the regions the crawler will crawl resources for\n     */\n    public EddaEBSVolumeJanitorCrawler(EddaClient eddaClient, String... regions) {\n        Validate.notNull(eddaClient);\n        this.eddaClient = eddaClient;\n        Validate.notNull(regions);\n        for (String region : regions) {\n            this.regions.add(region);\n            updateInstanceToOwner(region);\n        }\n        LOGGER.info(String.format(\"Found owner for %d instances in %s\", instanceToOwner.size(), this.regions));\n    }\n\n    private void updateInstanceToOwner(String region) {\n        LOGGER.info(String.format(\"Getting owners for all instances in region %s\", region));\n\n        long startTime = DateTime.now().minusDays(LOOKBACK_DAYS).getMillis();\n        String url = String.format(\"%1$s/view/instances;_since=%2$d;state.name=running;tags.key=%3$s;\"\n                + \"_expand:(instanceId,tags:(key,value))\",\n                eddaClient.getBaseUrl(region), startTime, BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for instance owners in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            JsonNode elem = it.next();\n            String instanceId = elem.get(\"instanceId\").getTextValue();\n            JsonNode tags = elem.get(\"tags\");\n            if (tags == null || !tags.isArray() || tags.size() == 0) {\n                continue;\n            }\n            for (Iterator<JsonNode> tagsIt = tags.getElements(); tagsIt.hasNext();) {\n                JsonNode tag = tagsIt.next();\n                String tagKey = tag.get(\"key\").getTextValue();\n                if (BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY.equals(tagKey)) {\n                    instanceToOwner.put(instanceId, tag.get(\"value\").getTextValue());\n                    break;\n                }\n            }\n        }\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.EBS_VOLUME);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"EBS_VOLUME\".equals(resourceType.name())) {\n            return getVolumeResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getVolumeResources(resourceIds);\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        Validate.notNull(resource);\n        return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n    }\n\n    private List<Resource> getVolumeResources(String... volumeIds) {\n        List<Resource> resources = Lists.newArrayList();\n        for (String region : regions) {\n            resources.addAll(getUnattachedVolumeResourcesInRegion(region, volumeIds));\n            addLastAttachmentInfo(resources);\n        }\n        return resources;\n    }\n\n    /**\n     * Gets all volumes that are not attached to any instance. Janitor Monkey only considers unattached volumes\n     * as cleanup candidates, so there is no need to get volumes that are in-use.\n     * @param region\n     * @return list of resources that are not attached to any instance\n     */\n    private List<Resource> getUnattachedVolumeResourcesInRegion(String region, String... volumeIds) {\n        String url = eddaClient.getBaseUrl(region) + \"/aws/volumes;\";\n        if (volumeIds != null && volumeIds.length != 0) {\n            url += StringUtils.join(volumeIds, ',');\n            LOGGER.info(String.format(\"Getting volumes in region %s for %d ids\", region, volumeIds.length));\n        } else {\n            LOGGER.info(String.format(\"Getting all unattached volumes in region %s\", region));\n        }\n        url += \";state=available;_expand:(volumeId,createTime,size,state,tags)\";\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for unattached volumes in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        List<Resource> resources = Lists.newArrayList();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            resources.add(parseJsonElementToVolumeResource(region, it.next()));\n        }\n        return resources;\n    }\n\n    private Resource parseJsonElementToVolumeResource(String region, JsonNode jsonNode) {\n        Validate.notNull(jsonNode);\n        long createTime = jsonNode.get(\"createTime\").asLong();\n\n        Resource resource = new AWSResource().withId(jsonNode.get(\"volumeId\").getTextValue()).withRegion(region)\n                .withResourceType(AWSResourceType.EBS_VOLUME)\n                .withLaunchTime(new Date(createTime));\n\n        JsonNode tags = jsonNode.get(\"tags\");\n        StringBuilder description = new StringBuilder();\n        JsonNode size = jsonNode.get(\"size\");\n        description.append(String.format(\"size=%s\", size == null ? \"unknown\" : size.getIntValue()));\n\n        if (tags == null || !tags.isArray() || tags.size() == 0) {\n            LOGGER.debug(String.format(\"No tags is found for %s\", resource.getId()));\n        } else {\n            for (Iterator<JsonNode> it = tags.getElements(); it.hasNext();) {\n                JsonNode tag = it.next();\n                String key = tag.get(\"key\").getTextValue();\n                String value = tag.get(\"value\").getTextValue();\n                description.append(String.format(\"; %s=%s\", key, value));\n                resource.setTag(key, value);\n                if (key.equals(PURPOSE)) {\n                    resource.setAdditionalField(PURPOSE, value);\n                }\n            }\n            resource.setDescription(description.toString());\n        }\n        ((AWSResource) resource).setAWSResourceState(jsonNode.get(\"state\").getTextValue());\n        return resource;\n    }\n\n\n    /**\n     * Adds information of last attachment to the resources. To be compatible with the AWS implementation of\n     * the same crawler, add the information to the JANITOR_META tag. It always uses the latest information\n     * to update the tag in this resource (not writing back to AWS) no matter if the tag exists.\n     * @param resources the volume resources\n     */\n    private void addLastAttachmentInfo(List<Resource> resources) {\n        Validate.notNull(resources);\n        LOGGER.info(String.format(\"Updating the latest attachment info for %d resources\", resources.size()));\n        Map<String, List<Resource>> regionToResources = Maps.newHashMap();\n        for (Resource resource : resources) {\n            List<Resource> regionalList = regionToResources.get(resource.getRegion());\n            if (regionalList == null) {\n                regionalList = Lists.newArrayList();\n                regionToResources.put(resource.getRegion(), regionalList);\n            }\n            regionalList.add(resource);\n        }\n        for (Map.Entry<String, List<Resource>> entry : regionToResources.entrySet()) {\n            LOGGER.info(String.format(\"Updating the latest attachment info for %d resources in region %s\",\n                    resources.size(), entry.getKey()));\n            for (List<Resource> batch : Lists.partition(entry.getValue(), BATCH_SIZE)) {\n                LOGGER.info(String.format(\"Processing batch of size %d\", batch.size()));\n                String batchUrl = getBatchUrl(entry.getKey(), batch);\n                JsonNode batchResult = null;\n                try {\n                    batchResult = eddaClient.getJsonNodeFromUrl(batchUrl);\n                } catch (IOException e) {\n                    LOGGER.error(\"Failed to get response for the batch.\", e);\n                }\n                Map<String, Resource> idToResource = Maps.newHashMap();\n                for (Resource resource : batch) {\n                    idToResource.put(resource.getId(), resource);\n\n                }\n                if (batchResult == null || !batchResult.isArray()) {\n                    throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\",\n                            batchUrl, batchResult));\n                }\n\n                Set<String> processedIds = Sets.newHashSet();\n                for (Iterator<JsonNode> it = batchResult.getElements(); it.hasNext();) {\n                    JsonNode elem = it.next();\n                    JsonNode data = elem.get(\"data\");\n                    String volumeId = data.get(\"volumeId\").getTextValue();\n                    Resource resource = idToResource.get(volumeId);\n                    JsonNode attachments = data.get(\"attachments\");\n\n                    if (!(attachments.isArray() && attachments.size() > 0)) {\n                        continue;\n                    }\n                    JsonNode attachment = attachments.get(0);\n\n                    JsonNode ltime = elem.get(\"ltime\");\n                    if (ltime == null || ltime.isNull()) {\n                        continue;\n                    }\n                    DateTime detachTime = new DateTime(ltime.asLong());\n                    processedIds.add(volumeId);\n                    setAttachmentInfo(volumeId, attachment, detachTime, resource);\n                }\n\n                for (Map.Entry<String, Resource> volumeEntry : idToResource.entrySet()) {\n                    String id = volumeEntry.getKey();\n                    if (!processedIds.contains(id)) {\n                        Resource resource = volumeEntry.getValue();\n                        LOGGER.info(String.format(\"Volume %s never was attached, use createTime %s as the detachTime\",\n                                id, resource.getLaunchTime()));\n                        setAttachmentInfo(id, null, new DateTime(resource.getLaunchTime().getTime()), resource);\n                    }\n                }\n            }\n        }\n    }\n\n    private void setAttachmentInfo(String volumeId, JsonNode attachment, DateTime detachTime, Resource resource) {\n        String instanceId = null;\n        if (attachment != null) {\n            boolean deleteOnTermination = attachment.get(DELETE_ON_TERMINATION).getBooleanValue();\n            if (deleteOnTermination) {\n                LOGGER.info(String.format(\n                        \"Volume %s had set the deleteOnTermination flag as true\", volumeId));\n            }\n            resource.setAdditionalField(DELETE_ON_TERMINATION, String.valueOf(deleteOnTermination));\n            instanceId = attachment.get(\"instanceId\").getTextValue();\n        }\n        // The subclass can customize the way to get the owner for a volume\n        String owner = getOwnerEmailForResource(resource);\n        if (owner == null && instanceId != null) {\n            owner = instanceToOwner.get(instanceId);\n        }\n        resource.setOwnerEmail(owner);\n\n        String metaTag = makeMetaTag(instanceId, owner, detachTime);\n        LOGGER.info(String.format(\"Setting Janitor Metatag as %s for volume %s\", metaTag, volumeId));\n        resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag);\n\n        LOGGER.info(String.format(\"The last detach time of volume %s is %s\", volumeId, detachTime));\n        resource.setAdditionalField(DETACH_TIME, String.valueOf(detachTime.getMillis()));\n    }\n\n    private String makeMetaTag(String instance, String owner, DateTime lastDetachTime) {\n        StringBuilder meta = new StringBuilder();\n        meta.append(String.format(\"%s=%s;\",\n                JanitorMonkey.INSTANCE_TAG_KEY, instance == null ? \"\" : instance));\n        meta.append(String.format(\"%s=%s;\", BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY, owner == null ? \"\" : owner));\n        meta.append(String.format(\"%s=%s\", JanitorMonkey.DETACH_TIME_TAG_KEY,\n                lastDetachTime == null ? \"\" : AWSResource.DATE_FORMATTER.print(lastDetachTime)));\n        return meta.toString();\n    }\n\n\n    private String getBatchUrl(String region, List<Resource> batch) {\n        StringBuilder batchUrl = new StringBuilder(eddaClient.getBaseUrl(region) + \"/aws/volumes/\");\n        boolean isFirst = true;\n        for (Resource resource : batch) {\n            if (!isFirst) {\n                batchUrl.append(',');\n            } else {\n                isFirst = false;\n            }\n            batchUrl.append(resource.getId());\n        }\n        batchUrl.append(\";data.state=in-use;_since=0;_expand;_meta:\"\n                + \"(ltime,data:(volumeId,attachments:(deleteOnTermination,instanceId)))\");\n        return batchUrl.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaELBJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler.edda;\n\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport com.netflix.simianarmy.janitor.JanitorCrawler;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.codehaus.jackson.JsonNode;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.*;\n\n/**\n * The crawler to crawl AWS instances for janitor monkey using Edda.\n */\npublic class EddaELBJanitorCrawler implements JanitorCrawler {\n\n    class DNSEntry {\n        String dnsName;\n        String dnsType;\n        String hostedZoneId;\n    };\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaELBJanitorCrawler.class);\n\n    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormat.forPattern(\"yyyy-MM-dd'T'HH:mm:ss.S'Z'\");\n\n    private final EddaClient eddaClient;\n    private final List<String> regions = Lists.newArrayList();\n    private final boolean useEddaApplicationOwner;\n    private final String fallbackOwnerEmail;\n\n    private Map<String, String> applicationToOwner = new HashMap<String, String>();\n\n    /**\n     * Instantiates a new basic instance crawler.\n     * @param eddaClient\n     *            the Edda client\n     * @param regions\n     *            the regions the crawler will crawl resources for\n     */\n    public EddaELBJanitorCrawler(EddaClient eddaClient, String fallbackOwnerEmail, boolean useEddaApplicationOwner, String... regions) {\n        this.useEddaApplicationOwner = useEddaApplicationOwner;\n        this.fallbackOwnerEmail = fallbackOwnerEmail;\n        Validate.notNull(eddaClient);\n        this.eddaClient = eddaClient;\n        Validate.notNull(regions);\n        for (String region : regions) {\n            this.regions.add(region);\n        }\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.ELB);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"ELB\".equals(resourceType.name())) {\n            return getELBResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getELBResources(resourceIds);\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        Validate.notNull(resource);\n        String ownerEmail = null;\n        if (useEddaApplicationOwner) {\n            for (String app : applicationToOwner.keySet()) {\n                if (resource.getId().toLowerCase().startsWith(app)) {\n                    ownerEmail = applicationToOwner.get(app);\n                    break;\n                }\n            }\n        } else {\n            ownerEmail = resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n        }\n\n        if (ownerEmail == null) {\n            ownerEmail = fallbackOwnerEmail;\n        }\n\n        return ownerEmail;\n    }\n\n    private List<Resource> getELBResources(String... instanceIds) {\n        if (useEddaApplicationOwner) {\n            applicationToOwner = EddaUtils.getAllApplicationOwnerEmails(eddaClient);\n        }\n\n        List<Resource> resources = Lists.newArrayList();\n        for (String region : regions) {\n            resources.addAll(getELBResourcesInRegion(region, instanceIds));\n        }\n        return resources;\n    }\n\n    private List<Resource> getELBResourcesInRegion(String region, String... elbNames) {\n        String url = eddaClient.getBaseUrl(region) + \"/aws/loadBalancers\";\n        if (elbNames != null && elbNames.length != 0) {\n            url += StringUtils.join(elbNames, ',');\n            LOGGER.info(String.format(\"Getting ELBs in region %s for %d names\", region, elbNames.length));\n        } else {\n            LOGGER.info(String.format(\"Getting all ELBs in region %s\", region));\n        }\n\n        url += \";_expand:(loadBalancerName,createdTime,DNSName,instances,tags:(key,value))\";\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for ELBs in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n\n        List<Resource> resources = Lists.newArrayList();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            resources.add(parseJsonElementToELBResource(region, it.next()));\n        }\n\n        Map<String, List<String>> elBtoASGMap = buildELBtoASGMap(region);\n        for(Resource resource : resources) {\n            List<String> asgList = elBtoASGMap.get(resource.getId());\n            if (asgList != null && asgList.size() > 0) {\n                resource.setAdditionalField(\"referencedASGCount\", \"\" + asgList.size());\n                String asgStr = StringUtils.join(asgList,\",\");\n                resource.setDescription(resource.getDescription() + \", ASGS=\" + asgStr);\n                LOGGER.debug(String.format(\"Resource ELB %s is referenced by ASGs %s\", resource.getId(), asgStr));\n            } else {\n                resource.setAdditionalField(\"referencedASGCount\", \"0\");\n                resource.setDescription(resource.getDescription() + \", ASGS=none\");\n                LOGGER.debug(String.format(\"No ASGs found for ELB %s\", resource.getId()));\n            }\n        }\n\n        Map<String, List<DNSEntry>> elBtoDNSMap = buildELBtoDNSMap(region);\n        for(Resource resource : resources) {\n            List<DNSEntry> dnsEntryList = elBtoDNSMap.get(resource.getAdditionalField(\"DNSName\"));\n            if (dnsEntryList != null && dnsEntryList.size() > 0) {\n                ArrayList<String> dnsNames = new ArrayList<>();\n                ArrayList<String> dnsTypes = new ArrayList<>();\n                ArrayList<String> hostedZoneIds = new ArrayList<>();\n                for (DNSEntry dnsEntry : dnsEntryList) {\n                    dnsNames.add(dnsEntry.dnsName);\n                    dnsTypes.add(dnsEntry.dnsType);\n                    hostedZoneIds.add(dnsEntry.hostedZoneId);\n                }\n\n                resource.setAdditionalField(\"referencedDNS\", StringUtils.join(dnsNames,\",\"));\n                resource.setAdditionalField(\"referencedDNSTypes\", StringUtils.join(dnsTypes,\",\"));\n                resource.setAdditionalField(\"referencedDNSZones\", StringUtils.join(hostedZoneIds,\",\"));\n\n                resource.setDescription(resource.getDescription() + \", DNS=\" + resource.getAdditionalField(\"referencedDNS\"));\n                LOGGER.debug(String.format(\"Resource ELB %s is referenced by DNS %s\", resource.getId(), resource.getAdditionalField(\"referencedDNS\")));\n            } else {\n                resource.setAdditionalField(\"referencedDNS\", \"\");\n                resource.setDescription(resource.getDescription() + \", DNS=none\");\n                LOGGER.debug(String.format(\"No DNS found for ELB %s\", resource.getId()));\n            }\n        }\n\n        return resources;\n    }\n\n    private Map<String, List<String>> buildELBtoASGMap(String region) {\n        String url = eddaClient.getBaseUrl(region) + \"/aws/autoScalingGroups;_expand:(autoScalingGroupName,loadBalancerNames)\";\n        LOGGER.info(String.format(\"Getting all ELBs associated with ASGs in region %s\", region));\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get JSON node from edda for ASG ELBs in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        HashMap<String, List<String>> asgMap = new HashMap<>();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            JsonNode asgNode = it.next();\n            String asgName = asgNode.get(\"autoScalingGroupName\").getTextValue();\n            JsonNode elbs = asgNode.get(\"loadBalancerNames\");\n            if (elbs == null || !elbs.isArray() || elbs.size() == 0) {\n                continue;\n            } else {\n                for (Iterator<JsonNode> elbNode = elbs.getElements(); elbNode.hasNext();) {\n                    JsonNode elb = elbNode.next();\n                    String elbName = elb.getTextValue();\n\n                    List<String> asgList = asgMap.get(elbName);\n                    if (asgList == null) {\n                        asgList = new ArrayList<>();\n                        asgMap.put(elbName, asgList);\n                    }\n                    asgList.add(asgName);\n                    LOGGER.debug(String.format(\"Found ASG %s associated with ELB %s\", asgName, elbName));\n                }\n            }\n        }\n        return asgMap;\n    }\n\n    private Resource parseJsonElementToELBResource(String region, JsonNode jsonNode) {\n        Validate.notNull(jsonNode);\n\n        String elbName = jsonNode.get(\"loadBalancerName\").getTextValue();\n        long launchTime = jsonNode.get(\"createdTime\").getLongValue();\n\n        Resource resource = new AWSResource().withId(elbName).withRegion(region)\n                .withResourceType(AWSResourceType.ELB)\n                .withLaunchTime(new Date(launchTime));\n\n        String dnsName = jsonNode.get(\"DNSName\").getTextValue();\n        resource.setAdditionalField(\"DNSName\", dnsName);\n\n        JsonNode tags = jsonNode.get(\"tags\");\n        if (tags == null || !tags.isArray() || tags.size() == 0) {\n            LOGGER.debug(String.format(\"No tags is found for %s\", resource.getId()));\n        } else {\n            for (Iterator<JsonNode> it = tags.getElements(); it.hasNext();) {\n                JsonNode tag = it.next();\n                String key = tag.get(\"key\").getTextValue();\n                String value = tag.get(\"value\").getTextValue();\n                resource.setTag(key, value);\n            }\n        }\n\n        String owner = getOwnerEmailForResource(resource);\n        if (owner != null) {\n            resource.setOwnerEmail(owner);\n        }\n        LOGGER.debug(String.format(\"Owner of ELB Resource %s (ELB DNS: %s) is %s\", resource.getId(), resource.getAdditionalField(\"DNSName\"), resource.getOwnerEmail()));\n\n        JsonNode instances = jsonNode.get(\"instances\");\n        if (instances == null || !instances.isArray() || instances.size() == 0) {\n            resource.setAdditionalField(\"instanceCount\", \"0\");\n            resource.setDescription(\"instances=none\");\n            LOGGER.debug(String.format(\"No instances found for ELB %s\", resource.getId()));\n        } else {\n            resource.setAdditionalField(\"instanceCount\", \"\" + instances.size());\n            ArrayList<String> instanceList = new ArrayList<String>(instances.size());\n            LOGGER.debug(String.format(\"Found %d instances for ELB %s\", instances.size(), resource.getId()));\n            for (Iterator<JsonNode> it = instances.getElements(); it.hasNext();) {\n                JsonNode instance = it.next();\n                String instanceId = instance.get(\"instanceId\").getTextValue();\n                instanceList.add(instanceId);\n            }\n            String instancesStr = StringUtils.join(instanceList,\",\");\n            resource.setDescription(String.format(\"instances=%s\", instances));\n            LOGGER.debug(String.format(\"Resource ELB %s has instances %s\", resource.getId(), instancesStr));\n        }\n\n        return resource;\n    }\n\n    private Map<String, List<DNSEntry>> buildELBtoDNSMap(String region) {\n        String url = eddaClient.getBaseUrl(region) + \"/aws/hostedRecords;_expand:(name,type,aliasTarget,resourceRecords:(value),zone:(id))\";\n        LOGGER.info(String.format(\"Getting all ELBs associated with DNSs in region %s\", region));\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get JSON node from edda for DNS ELBs in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        HashMap<String, List<DNSEntry>> dnsMap = new HashMap<>();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            JsonNode dnsNode = it.next();\n            String dnsName = dnsNode.get(\"name\").getTextValue();\n            String dnsType = dnsNode.get(\"type\").getTextValue();\n            String hostedZoneId = null;\n            JsonNode hostedZoneNode = dnsNode.get(\"zone\");\n            if (hostedZoneNode  != null) {\n                JsonNode hostedZoneIdNode = hostedZoneNode.get(\"id\");\n                if (hostedZoneIdNode != null) {\n                    hostedZoneId = hostedZoneIdNode.getTextValue();\n                }\n            }\n\n            JsonNode aliasTarget = dnsNode.get(\"aliasTarget\");\n            if (aliasTarget != null) {\n                JsonNode aliasTargetDnsNameNode = aliasTarget.get(\"DNSName\");\n                if (aliasTargetDnsNameNode != null) {\n                    String aliasTargetDnsName = aliasTargetDnsNameNode.getTextValue();\n                    if (aliasTargetDnsName != null && aliasTargetDnsName.contains(\".elb.\")) {\n                        DNSEntry dnsEntry = new DNSEntry();\n                        dnsEntry.dnsName = dnsName;\n                        dnsEntry.dnsType = dnsType;\n                        dnsEntry.hostedZoneId = hostedZoneId;\n\n                        if (aliasTargetDnsName.endsWith(\".\")) {\n                            aliasTargetDnsName = aliasTargetDnsName.substring(0, aliasTargetDnsName.length()-1);\n                        }\n                        List<DNSEntry> dnsEntryList = dnsMap.get(aliasTargetDnsName);\n                        if (dnsEntryList == null) {\n                            dnsEntryList = new ArrayList<>();\n                            dnsMap.put(aliasTargetDnsName, dnsEntryList);\n                        }\n                        dnsEntryList.add(dnsEntry);\n                        LOGGER.debug(String.format(\"Found DNS %s (alias) associated with ELB DNS %s, type %s, zone %s\", dnsName, aliasTargetDnsName, dnsType, hostedZoneId));\n                    }\n                }\n            }\n            JsonNode records = dnsNode.get(\"resourceRecords\");\n            if (records == null || !records.isArray() || records.size() == 0) {\n                continue;\n            } else {\n                for (Iterator<JsonNode> recordNode = records.getElements(); recordNode.hasNext();) {\n                    JsonNode record = recordNode.next();\n                    String elbDNS = record.get(\"value\").getTextValue();\n                    if (elbDNS.contains(\".elb.\")) {\n                        DNSEntry dnsEntry = new DNSEntry();\n                        dnsEntry.dnsName = dnsName;\n                        dnsEntry.dnsType = dnsType;\n                        dnsEntry.hostedZoneId = hostedZoneId;\n\n                        List<DNSEntry> dnsEntryList = dnsMap.get(elbDNS);\n                        if (dnsEntryList == null) {\n                            dnsEntryList = new ArrayList<>();\n                            dnsMap.put(elbDNS, dnsEntryList);\n                        }\n                        dnsEntryList.add(dnsEntry);\n                        LOGGER.debug(String.format(\"Found DNS %s associated with ELB DNS %s, type %s, zone %s\", dnsName, elbDNS, dnsType, hostedZoneId));\n                    }\n                }\n            }\n        }\n        return dnsMap;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaImageJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler.edda;\n\nimport com.google.common.collect.Iterables;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.common.collect.Sets;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport com.netflix.simianarmy.janitor.JanitorCrawler;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.codehaus.jackson.JsonNode;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * The crawler to crawl AWS AMIs for janitor monkey using Edda. Only images that are not currently referenced\n * by any existing instances or launch configurations are returned.\n */\npublic class EddaImageJanitorCrawler implements JanitorCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaImageJanitorCrawler.class);\n\n    /** The name representing the additional field name for the last reference time by instance. */\n    public static final String AMI_FIELD_LAST_INSTANCE_REF_TIME = \"Last_Instance_Reference_Time\";\n\n    /** The name representing the additional field name for the last reference time by launch config. */\n    public static final String AMI_FIELD_LAST_LC_REF_TIME = \"Last_Launch_Config_Reference_Time\";\n\n    /** The name representing the additional field name for whether the image is a base image. **/\n    public static final String AMI_FIELD_BASE_IMAGE = \"Base_Image\";\n\n    private static final int BATCH_SIZE = 100;\n\n    private final EddaClient eddaClient;\n    private final List<String> regions = Lists.newArrayList();\n\n    private final Set<String> usedByInstance = Sets.newHashSet();\n    private final Set<String> usedByLaunchConfig = Sets.newHashSet();\n    private final Set<String> usedNames = Sets.newHashSet();\n    protected final Map<String, String> imageIdToName = Maps.newHashMap();\n    private final Map<String, Long> imageIdToCreationTime = Maps.newHashMap();\n    private final Set<String> ancestorImageIds = Sets.newHashSet();\n\n    private String ownerId;\n    private final int daysBack;\n\n    private static final String IMAGE_ID = \"ami-[a-z0-9]{8}\";\n    private static final Pattern BASE_AMI_ID_PATTERN = Pattern.compile(\"^.*?base_ami_id=(\" + IMAGE_ID + \").*?\");\n    private static final Pattern ANCESTOR_ID_PATTERN = Pattern.compile(\"^.*?ancestor_id=(\" + IMAGE_ID + \").*?$\");\n\n    /**\n     * Instantiates a new basic AMI crawler.\n     * @param eddaClient\n     *            the Edda client\n     * @param daysBack\n     *            the number of days that the crawler checks back in history stored in Edda\n     * @param regions\n     *            the regions the crawler will crawl resources for\n     */\n    public EddaImageJanitorCrawler(EddaClient eddaClient, String ownerId, int daysBack, String... regions) {\n        Validate.notNull(eddaClient);\n        this.eddaClient = eddaClient;\n        this.ownerId = ownerId;\n        Validate.isTrue(daysBack >= 0);\n        this.daysBack = daysBack;\n        Validate.notNull(regions);\n        for (String region : regions) {\n            this.regions.add(region);\n        }\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.IMAGE);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"IMAGE\".equals(resourceType.name())) {\n            return getAMIResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... imageIds) {\n        return getAMIResources(imageIds);\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        Validate.notNull(resource);\n        return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n    }\n\n    private List<Resource> getAMIResources(String... imageIds) {\n        refreshIdToNameMap();\n        refreshAMIsUsedByInstance();\n        refreshAMIsUsedByLC();\n        refreshIdToCreationTime();\n        for (String excludedId : getExcludedImageIds()) {\n            String name = imageIdToName.get(excludedId);\n            usedNames.add(name);\n        }\n        LOGGER.info(String.format(\"%d image names are used across the %d regions.\",\n                usedNames.size(), regions.size()));\n        Collection<String> excludedImageIds = getExcludedImageIds();\n        List<Resource> resources = Lists.newArrayList();\n        for (String region : regions) {\n            try {\n                resources.addAll(getAMIResourcesInRegion(region, excludedImageIds, imageIds));\n            } catch (Exception e) {\n                LOGGER.error(\"AMI look up failed for {} in {}\", imageIds, region, e);\n            }\n        }\n        return resources;\n    }\n\n    /**\n     * The method allows users to put their own logic to exclude a set of images from being\n     * cleaned up by Janitor Monkey. In some cases, images are not used but still need to be\n     * kept longer.\n     * @return a collection of image ids that need to be excluded from Janitor Monkey\n     */\n    protected Collection<String> getExcludedImageIds() {\n        return Sets.newHashSet();\n    }\n\n    private JsonNode getImagesInJson(String region, String... imageIds) {\n        String url = eddaClient.getBaseUrl(region) + \"/aws/images\";\n        if (imageIds != null && imageIds.length != 0) {\n            url += \"/\" + StringUtils.join(imageIds, ',');\n            if (imageIds.length == 1) {\n                url +=\",\"; // Edda will return a non-array if passing exactly one imageId which will fail the crawler\n            }\n            LOGGER.info(String.format(\"Getting unreferenced AMIs in region %s for %d ids\", region, imageIds.length));\n        } else {\n            LOGGER.info(String.format(\"Getting all unreferenced AMIs in region %s\", region));\n            if (StringUtils.isNotBlank(ownerId)) {\n                url += \";ownerId=\" + ownerId;\n            }\n        }\n        url += \";_expand:(imageId,name,description,state,tags:(key,value))\";\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for AMIs in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n        return jsonNode;\n    }\n\n    private void refreshIdToNameMap() {\n        imageIdToName.clear();\n        for (String region : regions) {\n            JsonNode jsonNode = getImagesInJson(region);\n            for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n                JsonNode ami = it.next();\n                String imageId = ami.get(\"imageId\").getTextValue();\n                String name = ami.get(\"name\").getTextValue();\n                imageIdToName.put(imageId, name);\n            }\n        }\n        LOGGER.info(String.format(\"Got mapping from image id to name for %d ids\", imageIdToName.size()));\n    }\n\n    /**\n     * AWS doesn't provide creation time for images. We use the ctime (the creation time of the image record in Edda)\n     * to approximate the creation time of the image.\n     */\n    private void refreshIdToCreationTime() {\n        for (String region : regions) {\n            String url = eddaClient.getBaseUrl(region) + \"/aws/images\";\n            LOGGER.info(String.format(\"Getting the creation time for all AMIs in region %s\", region));\n            if (StringUtils.isNotBlank(ownerId)) {\n                url += \";data.ownerId=\" + ownerId;\n            }\n            url += \";_expand;_meta:(ctime,data:(imageId))\";\n\n            JsonNode jsonNode = null;\n            try {\n                jsonNode = eddaClient.getJsonNodeFromUrl(url);\n            } catch (Exception e) {\n                LOGGER.error(String.format(\n                        \"Failed to get Jason node from edda for creation time of AMIs in region %s.\", region), e);\n            }\n\n            if (jsonNode == null || !jsonNode.isArray()) {\n                throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\",\n                        url, jsonNode));\n            }\n\n            for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n                JsonNode elem = it.next();\n                JsonNode data = elem.get(\"data\");\n                String imageId = data.get(\"imageId\").getTextValue();\n                JsonNode ctimeNode = elem.get(\"ctime\");\n                if (ctimeNode != null && !ctimeNode.isNull()) {\n                    long ctime = ctimeNode.asLong();\n                    LOGGER.debug(String.format(\"The image record of %s was created in Edda at %s\",\n                            imageId, new DateTime(ctime)));\n                    imageIdToCreationTime.put(imageId, ctime);\n                }\n            }\n        }\n        LOGGER.info(String.format(\"Got creation time for %d images\", imageIdToCreationTime.size()));\n    }\n\n    private List<Resource> getAMIResourcesInRegion(String region,\n                                                   Collection<String> excludedImageIds,\n                                                   String... imageIds) {\n        JsonNode jsonNode = getImagesInJson(region, imageIds);\n        List<Resource> resources = Lists.newArrayList();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            JsonNode ami = it.next();\n            String imageId = ami.get(\"imageId\").getTextValue();\n            Resource resource = parseJsonElementToresource(region, ami);\n            String name = ami.get(\"name\").getTextValue();\n            if (excludedImageIds.contains(imageId)) {\n                LOGGER.info(String.format(\"Image %s is excluded from being managed by Janitor Monkey, ignore.\",\n                        imageId));\n                continue;\n            }\n            if (usedByInstance.contains(imageId) || usedByLaunchConfig.contains(imageId)) {\n                LOGGER.info(String.format(\"AMI %s is referenced by existing instance or launch configuration.\",\n                        imageId));\n            } else {\n                LOGGER.info(String.format(\"AMI %s is not referenced by existing instance or launch configuration.\",\n                        imageId));\n                if (usedNames.contains(name)) {\n                    LOGGER.info(String.format(\"The same AMI name %s is used in another region\", name));\n                } else {\n                    resources.add(resource);\n                }\n            }\n        }\n        long since = DateTime.now().minusDays(daysBack).getMillis();\n        addLastReferenceInfo(resources, since);\n\n        // Mark the base AMIs that are used as the ancestor of other images\n        for (Resource resource : resources) {\n            if (ancestorImageIds.contains(resource.getId())) {\n                resource.setAdditionalField(AMI_FIELD_BASE_IMAGE, \"true\");\n            }\n        }\n\n        return resources;\n    }\n\n    private Resource parseJsonElementToresource(String region, JsonNode jsonNode) {\n        Validate.notNull(jsonNode);\n\n        String imageId = jsonNode.get(\"imageId\").getTextValue();\n\n        Resource resource = new AWSResource().withId(imageId).withRegion(region)\n                .withResourceType(AWSResourceType.IMAGE);\n\n        Long creationTime = imageIdToCreationTime.get(imageId);\n        if (creationTime != null) {\n            resource.setLaunchTime(new Date(creationTime));\n        }\n\n        JsonNode tags = jsonNode.get(\"tags\");\n        if (tags == null || !tags.isArray() || tags.size() == 0) {\n            LOGGER.debug(String.format(\"No tags is found for %s\", resource.getId()));\n        } else {\n            for (Iterator<JsonNode> it = tags.getElements(); it.hasNext();) {\n                JsonNode tag = it.next();\n                String key = tag.get(\"key\").getTextValue();\n                String value = tag.get(\"value\").getTextValue();\n                resource.setTag(key, value);\n            }\n        }\n\n        JsonNode descNode = jsonNode.get(\"description\");\n        if (descNode != null && !descNode.isNull()) {\n            String description = descNode.getTextValue();\n            resource.setDescription(description);\n            String ancestorImageId = getBaseAmiIdFromDescription(description);\n            if (ancestorImageId != null && !ancestorImageIds.contains(ancestorImageId)) {\n                LOGGER.info(String.format(\"Found base AMI id %s from description '%s'\", ancestorImageId, description));\n                ancestorImageIds.add(ancestorImageId);\n            }\n        }\n        ((AWSResource) resource).setAWSResourceState(jsonNode.get(\"state\").getTextValue());\n\n        String owner = getOwnerEmailForResource(resource);\n        if (owner != null) {\n            resource.setOwnerEmail(owner);\n        }\n        return resource;\n    }\n\n    private void refreshAMIsUsedByInstance() {\n        usedByInstance.clear();\n        for (String region : regions) {\n            LOGGER.info(String.format(\"Getting AMIs used by instances in region %s\", region));\n            String url = eddaClient.getBaseUrl(region) + \"/view/instances/;_expand:(imageId)\";\n\n            JsonNode jsonNode = null;\n            try {\n                jsonNode = eddaClient.getJsonNodeFromUrl(url);\n            } catch (Exception e) {\n                LOGGER.error(String.format(\n                        \"Failed to get Jason node from edda for AMIs used by instances in region %s.\", region), e);\n            }\n\n            if (jsonNode == null || !jsonNode.isArray()) {\n                throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\",\n                        url, jsonNode));\n            }\n\n            for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n                JsonNode img = it.next();\n                String id = img.get(\"imageId\").getTextValue();\n                usedByInstance.add(id);\n                usedNames.add(imageIdToName.get(id));\n            }\n        }\n        LOGGER.info(String.format(\"Found %d image ids used by instance from Edda\", usedByInstance.size()));\n    }\n\n    private void refreshAMIsUsedByLC() {\n        usedByLaunchConfig.clear();\n        for (String region : regions) {\n            LOGGER.info(String.format(\"Getting AMIs used by launch configs in region %s\", region));\n            String url = eddaClient.getBaseUrl(region) + \"/aws/launchConfigurations;_expand:(imageId)\";\n\n            JsonNode jsonNode = null;\n            try {\n                jsonNode = eddaClient.getJsonNodeFromUrl(url);\n            } catch (Exception e) {\n                LOGGER.error(String.format(\n                        \"Failed to get Jason node from edda for AMIs used by launch configs in region %s.\", region), e);\n            }\n\n            if (jsonNode == null || !jsonNode.isArray()) {\n                throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\",\n                        url, jsonNode));\n            }\n\n            for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n                JsonNode img = it.next();\n                String id = img.get(\"imageId\").getTextValue();\n                usedByLaunchConfig.add(id);\n                usedNames.add(imageIdToName.get(id));\n            }\n        }\n        LOGGER.info(String.format(\"Found %d image ids used by launch config from Edda\", usedByLaunchConfig.size()));\n    }\n\n    private void addLastReferenceInfo(List<Resource> resources, long since) {\n        Validate.notNull(resources);\n        LOGGER.info(String.format(\"Updating the latest reference info for %d images\", resources.size()));\n        Map<String, List<Resource>> regionToResources = Maps.newHashMap();\n        for (Resource resource : resources) {\n            List<Resource> regionalList = regionToResources.get(resource.getRegion());\n            if (regionalList == null) {\n                regionalList = Lists.newArrayList();\n                regionToResources.put(resource.getRegion(), regionalList);\n            }\n            regionalList.add(resource);\n        }\n        //\n        for (Map.Entry<String, List<Resource>> entry : regionToResources.entrySet()) {\n            String region = entry.getKey();\n            LOGGER.info(String.format(\"Updating the latest reference info for %d images in region %s\",\n                    resources.size(), region));\n            for (List<Resource> batch : Lists.partition(entry.getValue(), BATCH_SIZE)) {\n                LOGGER.info(String.format(\"Processing batch of size %d\", batch.size()));\n                updateReferenceTimeByInstance(region, batch, since);\n                updateReferenceTimeByLaunchConfig(region, batch, since);\n            }\n        }\n    }\n\n    private void updateReferenceTimeByInstance(String region, List<Resource> batch, long since) {\n        LOGGER.info(String.format(\"Getting the last reference time by instance for batch of size %d\", batch.size()));\n        String batchUrl = getInstanceBatchUrl(region, batch, since);\n        JsonNode batchResult = null;\n        Map<String, Resource> idToResource = Maps.newHashMap();\n        for (Resource resource : batch) {\n            idToResource.put(resource.getId(), resource);\n        }\n        try {\n            batchResult = eddaClient.getJsonNodeFromUrl(batchUrl);\n        } catch (IOException e) {\n            LOGGER.error(\"Failed to get response for the batch.\", e);\n        }\n        if (batchResult == null || !batchResult.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\",\n                    batchUrl, batchResult));\n        }\n        for (Iterator<JsonNode> it = batchResult.getElements(); it.hasNext();) {\n            JsonNode elem = it.next();\n            JsonNode data = elem.get(\"data\");\n            String imageId = data.get(\"imageId\").getTextValue();\n            String instanceId = data.get(\"instanceId\").getTextValue();\n            JsonNode ltimeNode = elem.get(\"ltime\");\n            if (ltimeNode != null && !ltimeNode.isNull()) {\n                long ltime = ltimeNode.asLong();\n                Resource ami = idToResource.get(imageId);\n                String lastRefTimeByInstance = ami.getAdditionalField(\n                        AMI_FIELD_LAST_INSTANCE_REF_TIME);\n                if (lastRefTimeByInstance == null || Long.parseLong(lastRefTimeByInstance) < ltime) {\n                    LOGGER.info(String.format(\"The last time that the image %s was referenced by instance %s is %d\",\n                            imageId, instanceId, ltime));\n                    ami.setAdditionalField(AMI_FIELD_LAST_INSTANCE_REF_TIME, String.valueOf(ltime));\n                }\n            }\n        }\n    }\n\n    private void updateReferenceTimeByLaunchConfig(String region, List<Resource> batch, long since) {\n        LOGGER.info(String.format(\"Getting the last reference time by launch config for batch of size %d\",\n                batch.size()));\n        String batchUrl = getLaunchConfigBatchUrl(region, batch, since);\n        JsonNode batchResult = null;\n        Map<String, Resource> idToResource = Maps.newHashMap();\n        for (Resource resource : batch) {\n            idToResource.put(resource.getId(), resource);\n        }\n        try {\n            batchResult = eddaClient.getJsonNodeFromUrl(batchUrl);\n        } catch (IOException e) {\n            LOGGER.error(\"Failed to get response for the batch.\", e);\n        }\n        if (batchResult == null || !batchResult.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\",\n                    batchUrl, batchResult));\n        }\n        for (Iterator<JsonNode> it = batchResult.getElements(); it.hasNext();) {\n            JsonNode elem = it.next();\n            JsonNode data = elem.get(\"data\");\n            String imageId = data.get(\"imageId\").getTextValue();\n            String launchConfigurationName = data.get(\"launchConfigurationName\").getTextValue();\n            JsonNode ltimeNode = elem.get(\"ltime\");\n            if (ltimeNode != null && !ltimeNode.isNull()) {\n                long ltime = ltimeNode.asLong();\n                Resource ami = idToResource.get(imageId);\n                String lastRefTimeByLC = ami.getAdditionalField(AMI_FIELD_LAST_LC_REF_TIME);\n                if (lastRefTimeByLC == null || Long.parseLong(lastRefTimeByLC) < ltime) {\n                    LOGGER.info(String.format(\n                            \"The last time that the image %s was referenced by launch config %s is %d\",\n                            imageId, launchConfigurationName, ltime));\n                    ami.setAdditionalField(AMI_FIELD_LAST_LC_REF_TIME, String.valueOf(ltime));\n                }\n            }\n        }\n    }\n\n    private String getInstanceBatchUrl(String region, List<Resource> batch, long since) {\n        StringBuilder batchUrl = new StringBuilder(eddaClient.getBaseUrl(region)\n                + \"/view/instances/;data.imageId=\");\n        batchUrl.append(getImageIdsString(batch));\n        batchUrl.append(String.format(\";data.state.name=terminated;_since=%d;_expand;_meta:\"\n                + \"(ltime,data:(imageId,instanceId))\", since));\n        return batchUrl.toString();\n    }\n\n    private String getLaunchConfigBatchUrl(String region, List<Resource> batch, long since) {\n        StringBuilder batchUrl = new StringBuilder(eddaClient.getBaseUrl(region)\n                + \"/aws/launchConfigurations/;data.imageId=\");\n        batchUrl.append(getImageIdsString(batch));\n        batchUrl.append(String.format(\";_since=%d;_expand;_meta:(ltime,data:(imageId,launchConfigurationName))\",\n                since));\n        return batchUrl.toString();\n    }\n\n    private String getImageIdsString(List<Resource> resources) {\n        StringBuilder sb = new StringBuilder();\n        boolean isFirst = true;\n        for (Resource resource : resources) {\n            if (!isFirst) {\n                sb.append(',');\n            } else {\n                isFirst = false;\n            }\n            sb.append(resource.getId());\n        }\n        return sb.toString();\n    }\n\n    private static String getBaseAmiIdFromDescription(String imageDescription) {\n        // base_ami_id=ami-1eb75c77,base_ami_name=servicenet-roku-qadd.dc.81210.10.44\n        Matcher matcher = BASE_AMI_ID_PATTERN.matcher(imageDescription);\n        if (matcher.matches()) {\n            return matcher.group(1);\n        }\n        // store=ebs,ancestor_name=ebs-centosbase-x86_64-20101124,ancestor_id=ami-7b4eb912\n        matcher = ANCESTOR_ID_PATTERN.matcher(imageDescription);\n        if (matcher.matches()) {\n            return matcher.group(1);\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaInstanceJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler.edda;\n\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport com.netflix.simianarmy.janitor.JanitorCrawler;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.codehaus.jackson.JsonNode;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.EnumSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.HashSet;\nimport java.util.HashMap;\n\n/**\n * The crawler to crawl AWS instances for janitor monkey using Edda.\n */\npublic class EddaInstanceJanitorCrawler implements JanitorCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaInstanceJanitorCrawler.class);\n\n    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormat.forPattern(\"yyyy-MM-dd'T'HH:mm:ss.S'Z'\");\n\n    private final EddaClient eddaClient;\n    private final List<String> regions = Lists.newArrayList();\n    private final Map<String, String> instanceToAsg = Maps.newHashMap();\n\n    /** Max image ids per Edda Query */\n    private static final int MAX_IMAGE_IDS_PER_QUERY = 40;\n\n    /**\n     * Instantiates a new basic instance crawler.\n     * @param eddaClient\n     *            the Edda client\n     * @param regions\n     *            the regions the crawler will crawl resources for\n     */\n    public EddaInstanceJanitorCrawler(EddaClient eddaClient, String... regions) {\n        Validate.notNull(eddaClient);\n        this.eddaClient = eddaClient;\n        Validate.notNull(regions);\n        for (String region : regions) {\n            this.regions.add(region);\n        }\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.INSTANCE);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"INSTANCE\".equals(resourceType.name())) {\n            return getInstanceResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getInstanceResources(resourceIds);\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        Validate.notNull(resource);\n        return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY);\n    }\n\n    private List<Resource> getInstanceResources(String... instanceIds) {\n        List<Resource> resources = Lists.newArrayList();\n        for (String region : regions) {\n            resources.addAll(getInstanceResourcesInRegion(region, instanceIds));\n        }\n        return resources;\n    }\n\n    private List<Resource> getInstanceResourcesInRegion(String region, String... instanceIds) {\n        refreshAsgInstances();\n\n        String url = eddaClient.getBaseUrl(region) + \"/view/instances;\";\n        if (instanceIds != null && instanceIds.length != 0) {\n            url += StringUtils.join(instanceIds, ',');\n            LOGGER.info(String.format(\"Getting instances in region %s for %d ids\", region, instanceIds.length));\n        } else {\n            LOGGER.info(String.format(\"Getting all instances in region %s\", region));\n        }\n        url += \";state.name=running;_expand:(instanceId,launchTime,state:(name),instanceType,imageId\"\n                + \",publicDnsName,tags:(key,value))\";\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for instances in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        List<Resource> resources = Lists.newArrayList();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            resources.add(parseJsonElementToInstanceResource(region, it.next()));\n        }\n        refreshOwnerByImage(region, resources);\n        return resources;\n    }\n\n    private Resource parseJsonElementToInstanceResource(String region, JsonNode jsonNode) {\n        Validate.notNull(jsonNode);\n\n        String instanceId = jsonNode.get(\"instanceId\").getTextValue();\n        long launchTime = jsonNode.get(\"launchTime\").getLongValue();\n\n        Resource resource = new AWSResource().withId(instanceId).withRegion(region)\n                .withResourceType(AWSResourceType.INSTANCE)\n                .withLaunchTime(new Date(launchTime));\n\n        JsonNode publicDnsName = jsonNode.get(\"publicDnsName\");\n        String description = String.format(\"type=%s; host=%s\",\n                jsonNode.get(\"instanceType\").getTextValue(),\n                publicDnsName == null ? \"\" : publicDnsName.getTextValue());\n        resource.setDescription(description);\n\n        String owner = getOwnerEmailForResource(resource);\n        resource.setOwnerEmail(owner);\n        JsonNode tags = jsonNode.get(\"tags\");\n        String asgName = null;\n        if (tags == null || !tags.isArray() || tags.size() == 0) {\n            LOGGER.debug(String.format(\"No tags is found for %s\", resource.getId()));\n        } else {\n            for (Iterator<JsonNode> it = tags.getElements(); it.hasNext();) {\n                JsonNode tag = it.next();\n                String key = tag.get(\"key\").getTextValue();\n                String value = tag.get(\"value\").getTextValue();\n                resource.setTag(key, value);\n                if (\"aws:autoscaling:groupName\".equals(key)) {\n                    asgName = value;\n                } else if (owner == null && BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY.equals(key)) {\n                    resource.setOwnerEmail(value);\n                }\n            }\n            resource.setDescription(description.toString());\n        }\n        // If we cannot find ASG name in tags, use the map for the ASG name\n        if (asgName == null) {\n            asgName = instanceToAsg.get(instanceId);\n            if (asgName != null) {\n                LOGGER.debug(String.format(\"Failed to find ASG name in tags of %s, use the ASG name %s from map\",\n                        instanceId, asgName));\n            }\n        }\n        if (asgName != null) {\n            resource.setAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME, asgName);\n        }\n        ((AWSResource) resource).setAWSResourceState(jsonNode.get(\"state\").get(\"name\").getTextValue());\n        String imageId = jsonNode.get(\"imageId\").getTextValue();\n        resource.setAdditionalField(\"imageId\", imageId);\n        return resource;\n    }\n\n    private void refreshAsgInstances() {\n        instanceToAsg.clear();\n        for (String region : regions) {\n            LOGGER.info(String.format(\"Getting ASG instances in region %s\", region));\n            String url = eddaClient.getBaseUrl(region) + \"/aws/autoScalingGroups\"\n                    + \";_expand:(autoScalingGroupName,instances:(instanceId))\";\n\n            JsonNode jsonNode = null;\n            try {\n                jsonNode = eddaClient.getJsonNodeFromUrl(url);\n            } catch (Exception e) {\n                LOGGER.error(String.format(\n                        \"Failed to get Jason node from edda for ASGs in region %s.\", region), e);\n            }\n\n            if (jsonNode == null || !jsonNode.isArray()) {\n                throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\",\n                        url, jsonNode));\n            }\n\n            for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n                JsonNode asg = it.next();\n                String asgName = asg.get(\"autoScalingGroupName\").getTextValue();\n                JsonNode instances = asg.get(\"instances\");\n                if (instances == null || instances.isNull() || !instances.isArray() || instances.size() == 0) {\n                    continue;\n                }\n                for (Iterator<JsonNode> instanceIt = instances.getElements(); instanceIt.hasNext();) {\n                    JsonNode instance = instanceIt.next();\n                    instanceToAsg.put(instance.get(\"instanceId\").getTextValue(), asgName);\n                }\n            }\n\n        }\n    }\n\n   private void refreshOwnerByImage(String region, List<Resource> resources) {\n        HashSet<String> imageIds = new HashSet<>();\n        for (Resource resource: resources) {\n            if (resource.getOwnerEmail() == null) {\n                imageIds.add(resource.getAdditionalField(\"imageId\"));\n            }\n        }\n        if  (imageIds.size() > 0) {\n            HashMap<String, String> imageToOwner = new HashMap<>();\n            String baseurl = eddaClient.getBaseUrl(region) + \"/aws/images/\";\n  \n            Iterator<String> itr = imageIds.iterator();\n            long leftToQuery = imageIds.size();\n            while (leftToQuery > 0) {\n                long batchcount = leftToQuery > MAX_IMAGE_IDS_PER_QUERY ? MAX_IMAGE_IDS_PER_QUERY : leftToQuery;\n                leftToQuery -= batchcount;\n              \n                ArrayList<String> batch = new ArrayList<>();\n                for(int i=0;i<batchcount; i++) {\n                    batch.add(itr.next());\n                }\n  \n                String url = baseurl;\n                url += StringUtils.join(batch, ',');\n                url += \";tags.key=owner;public=false;_expand:(imageId,tags:(owner))\";\n                JsonNode imageJsonNode = null;\n                try {\n                    imageJsonNode = eddaClient.getJsonNodeFromUrl(url);\n                } catch (Exception e) {\n                    LOGGER.error(String.format(\n                            \"Failed to get Json node from edda for AMIs in region %s.\", region), e);\n                }\n                \n                if (imageJsonNode != null) {\n                    for (Iterator<JsonNode> it = imageJsonNode.getElements(); it.hasNext();) {\n                        JsonNode image = it.next();\n                        String imageId = image.get(\"imageId\").getTextValue();\n                        JsonNode tags = image.get(\"tags\");\n                        for (Iterator<JsonNode> tagIt = tags.getElements(); tagIt.hasNext();) {\n                            JsonNode tag = tagIt.next();\n                            if (tag.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY) != null) {\n                                imageToOwner.put(imageId, tag.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY).getTextValue());\n                                break;\n                            }\n                        }\n                    }\n                }            \n            }  \n            \n            if (imageToOwner.size() > 0) {\n                for (Resource resource: resources) {\n                    if (resource.getOwnerEmail() == null\n                        && imageToOwner.get(resource.getAdditionalField(\"imageId\")) != null) {\n                        resource.setOwnerEmail(imageToOwner.get(resource.getAdditionalField(\"imageId\")));\n                        LOGGER.info(String.format(\"Found owner %s for instance %s in AMI %s\",\n                            resource.getOwnerEmail(), resource.getId(), resource.getAdditionalField(\"imageId\")));\n                    }\n                }\n            }\n        }       \n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaLaunchConfigJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler.edda;\n\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Sets;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport com.netflix.simianarmy.janitor.JanitorCrawler;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.codehaus.jackson.JsonNode;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.EnumSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * The crawler to crawl AWS launch configurations for janitor monkey using Edda.\n */\npublic class EddaLaunchConfigJanitorCrawler implements JanitorCrawler {\n\n    /** The name representing the additional field name of a flag indicating if the launch config\n     * if used by an auto scaling group. */\n    public static final String LAUNCH_CONFIG_FIELD_USED_BY_ASG = \"USED_BY_ASG\";\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaLaunchConfigJanitorCrawler.class);\n\n    private final EddaClient eddaClient;\n    private final List<String> regions = Lists.newArrayList();\n\n\n    /**\n     * Instantiates a new basic launch configuration crawler.\n     * @param eddaClient\n     *            the Edda client\n     * @param regions\n     *            the regions the crawler will crawl resources for\n     */\n    public EddaLaunchConfigJanitorCrawler(EddaClient eddaClient, String... regions) {\n        Validate.notNull(eddaClient);\n        this.eddaClient = eddaClient;\n        Validate.notNull(regions);\n        for (String region : regions) {\n            this.regions.add(region);\n        }\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(AWSResourceType.LAUNCH_CONFIG);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        if (\"LAUNCH_CONFIG\".equals(resourceType.name())) {\n            return getLaunchConfigResources();\n        }\n        return Collections.emptyList();\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        return getLaunchConfigResources(resourceIds);\n    }\n\n    private List<Resource> getLaunchConfigResources(String... launchConfigNames) {\n        List<Resource> resources = Lists.newArrayList();\n        for (String region : regions) {\n            resources.addAll(getLaunchConfigResourcesInRegion(region, launchConfigNames));\n        }\n        return resources;\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        //Launch Configs don't have Tags\n        return null;\n    }\n\n    private List<Resource> getLaunchConfigResourcesInRegion(String region, String... launchConfigNames) {\n        String url = eddaClient.getBaseUrl(region) + \"/aws/launchConfigurations;\";\n        if (launchConfigNames != null && launchConfigNames.length != 0) {\n            url += StringUtils.join(launchConfigNames, ',');\n            LOGGER.info(String.format(\"Getting launch configurations in region %s for %d ids\",\n                    region, launchConfigNames.length));\n        } else {\n            LOGGER.info(String.format(\"Getting all launch configurations in region %s\", region));\n        }\n        url += \";_expand:(launchConfigurationName,createdTime)\";\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for instances in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        List<Resource> resources = Lists.newArrayList();\n\n        Set<String> usedLCs = getLaunchConfigsInUse(region);\n\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            JsonNode launchConfiguration = it.next();\n            String lcName = launchConfiguration.get(\"launchConfigurationName\").getTextValue();\n            Resource lcResource = new AWSResource().withId(lcName)\n                    .withRegion(region).withResourceType(AWSResourceType.LAUNCH_CONFIG)\n                    .withLaunchTime(new Date(launchConfiguration.get(\"createdTime\").getLongValue()));\n            lcResource.setOwnerEmail(getOwnerEmailForResource(lcResource));\n\n            lcResource.setAdditionalField(LAUNCH_CONFIG_FIELD_USED_BY_ASG, String.valueOf(usedLCs.contains(lcName)));\n            resources.add(lcResource);\n        }\n        return resources;\n    }\n\n    /**\n     * Gets the launch configs that are currently in use by at least one ASG in a region.\n     * @param region the region\n     * @return the set of launch config names\n     */\n    private Set<String> getLaunchConfigsInUse(String region) {\n        LOGGER.info(String.format(\"Getting all launch configurations in use in region %s\", region));\n        String url = eddaClient.getBaseUrl(region) + \"/aws/autoScalingGroups;_expand:(launchConfigurationName)\";\n\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\n                    \"Failed to get Jason node from edda for launch configs in use in region %s.\", region), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"Failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        Set<String> launchConfigs = Sets.newHashSet();\n        for (Iterator<JsonNode> it = jsonNode.getElements(); it.hasNext();) {\n            launchConfigs.add(it.next().get(\"launchConfigurationName\").getTextValue());\n        }\n        return launchConfigs;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaUtils.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.crawler.edda;\n\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport org.codehaus.jackson.JsonNode;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.net.UnknownHostException;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * Misc common Edda Utilities\n */\npublic class EddaUtils {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaUtils.class);\n\n    public static Map<String, String> getAllApplicationOwnerEmails(EddaClient eddaClient) {\n        String region = \"us-east-1\";\n        LOGGER.info(String.format(\"Getting all application names and emails in region %s.\", region));\n\n        String url = eddaClient.getBaseUrl(region) + \"/netflix/applications/;_expand:(name,email)\";\n        JsonNode jsonNode = null;\n        try {\n            jsonNode = eddaClient.getJsonNodeFromUrl(url);\n        } catch (UnknownHostException e) {\n            LOGGER.warn(String.format(\"Edda endpoint is not available in region %s\", region));\n            return Collections.emptyMap();\n        } catch (Exception e) {\n            throw new RuntimeException(String.format(\"Failed to get Json node from url: %s\", url), e);\n        }\n\n        if (jsonNode == null || !jsonNode.isArray()) {\n            throw new RuntimeException(String.format(\"failed to get valid document from %s, got: %s\", url, jsonNode));\n        }\n\n        Iterator<JsonNode> it = jsonNode.getElements();\n        Map<String, String> appToOwner = new HashMap<String, String>();\n        while (it.hasNext()) {\n            JsonNode node = it.next();\n            String appName = node.get(\"name\").getTextValue().toLowerCase();\n            String owner = node.get(\"email\").getTextValue();\n            if (appName != null && owner != null) {\n                appToOwner.put(appName, owner);\n            }\n        }\n        return appToOwner;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/ami/UnusedImageRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.ami;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.janitor.crawler.edda.EddaImageJanitorCrawler;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Date;\n\n/**\n * The rule class to clean up images that are not used.\n */\npublic class UnusedImageRule  implements Rule {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(UnusedImageRule.class);\n\n    private final MonkeyCalendar calendar;\n    private final int retentionDays;\n    private final int lastReferenceDaysThreshold;\n\n    /**\n     * Constructor.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param retentionDays\n     *            The number of days that the marked ASG is retained before being terminated\n     * @param lastReferenceDaysThreshold\n     *            The number of days that the image has been not referenced that makes the ASG be\n     *            considered obsolete\n     */\n    public UnusedImageRule(MonkeyCalendar calendar, int retentionDays, int lastReferenceDaysThreshold) {\n        Validate.notNull(calendar);\n        Validate.isTrue(retentionDays >= 0);\n        Validate.isTrue(lastReferenceDaysThreshold >= 0);\n        this.calendar = calendar;\n        this.retentionDays = retentionDays;\n        this.lastReferenceDaysThreshold = lastReferenceDaysThreshold;\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!\"IMAGE\".equals(resource.getResourceType().name())) {\n            return true;\n        }\n        if (!\"available\".equals(((AWSResource) resource).getAWSResourceState())) {\n            return true;\n        }\n\n        if (\"true\".equals(resource.getAdditionalField(EddaImageJanitorCrawler.AMI_FIELD_BASE_IMAGE))) {\n            LOGGER.info(String.format(\"Image %s is a base image that is used to create other images\",\n                    resource.getId()));\n            return true;\n        }\n        long instanceRefTime = getRefTimeInMilis(resource, EddaImageJanitorCrawler.AMI_FIELD_LAST_INSTANCE_REF_TIME);\n        long lcRefTime = getRefTimeInMilis(resource, EddaImageJanitorCrawler.AMI_FIELD_LAST_LC_REF_TIME);\n        Date now = calendar.now().getTime();\n        long windowStart = new DateTime(now.getTime()).minusDays(lastReferenceDaysThreshold).getMillis();\n        if (instanceRefTime < windowStart && lcRefTime < windowStart) {\n            if (resource.getExpectedTerminationTime() == null) {\n                Date terminationTime = calendar.getBusinessDay(now, retentionDays);\n                resource.setExpectedTerminationTime(terminationTime);\n                resource.setTerminationReason(String.format(\"Image not referenced for %d days\",\n                        lastReferenceDaysThreshold + retentionDays));\n                LOGGER.info(String.format(\n                        \"Image %s in region %s is marked to be cleaned at %s as it is not referenced\"\n                               + \"for more than %d days\",\n                        resource.getId(), resource.getRegion(), resource.getExpectedTerminationTime(),\n                        lastReferenceDaysThreshold));\n            } else {\n                LOGGER.info(String.format(\"Resource %s is already marked.\", resource.getId()));\n            }\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Tries to get the long value from the provided field. If the field does not exist, try to use the\n     * creation time. If both do not exist, use the current time.\n     */\n    private long getRefTimeInMilis(Resource resource, String field) {\n        String fieldValue = resource.getAdditionalField(field);\n        long refTime;\n        if (fieldValue != null) {\n            refTime = Long.parseLong(fieldValue);\n        } else if (resource.getLaunchTime() != null) {\n            LOGGER.info(String.format(\"No value in field %s is found, use the creation time %s as the ref time of %s\",\n                    field, resource.getLaunchTime(), resource.getId()));\n            refTime = resource.getLaunchTime().getTime();\n        } else {\n            // When there is no creation time or ref time is found, we consider the image is referenced.\n            LOGGER.info(String.format(\"Use the current time as the ref time of %s\", resource.getId()));\n            refTime = DateTime.now().getMillis();\n        }\n        return refTime;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/ASGInstanceValidator.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.asg;\n\nimport com.netflix.simianarmy.Resource;\n\n/**\n * The interface is for checking whether an ASG has any active instance.\n */\npublic interface ASGInstanceValidator {\n    /**\n     * Checks whether an ASG resource contains any active instances.\n     * @param resource the ASG resource\n     * @return true if the ASG contains any active instances, false otherwise.\n     */\n    boolean hasActiveInstance(Resource resource);\n}"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/DiscoveryASGInstanceValidator.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.rule.asg;\n\nimport java.util.List;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.appinfo.InstanceInfo;\nimport com.netflix.appinfo.InstanceInfo.InstanceStatus;\nimport com.netflix.discovery.DiscoveryClient;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;\n\n/**\n * The class is for checking whether an ASG has any active instance using Discovery/Eureka.\n * If Discovery/Eureka is enabled, it uses its service to check if the instances in the ASG are\n * registered and up there.\n */\npublic class DiscoveryASGInstanceValidator implements ASGInstanceValidator {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryASGInstanceValidator.class);\n    private final DiscoveryClient discoveryClient;\n\n    /**\n     * Constructor.\n     * @param discoveryClient\n     *          the client to access the Discovery/Eureka service for checking the status of instances.\n     */\n    public DiscoveryASGInstanceValidator(DiscoveryClient discoveryClient) {\n        Validate.notNull(discoveryClient);\n        this.discoveryClient = discoveryClient;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean hasActiveInstance(Resource resource) {\n        String instanceIds = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES);\n        String maxSizeStr = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE);\n        if (StringUtils.isBlank(instanceIds)) {\n            if (maxSizeStr != null && Integer.parseInt(maxSizeStr) == 0) {\n                // The ASG is empty when it has no instance and the max size of the ASG is 0.\n                // If the max size is not 0, the ASG could probably be in the process of starting new instances.\n                LOGGER.info(String.format(\"ASG %s is empty.\", resource.getId()));\n                return false;\n            } else {\n                LOGGER.info(String.format(\"ASG %s does not have instances but the max size is %s\",\n                        resource.getId(), maxSizeStr));\n                return true;\n            }\n        }\n        String[] instances = StringUtils.split(instanceIds, \",\");\n        LOGGER.debug(String.format(\"Checking if the %d instances in ASG %s are active.\",\n                instances.length, resource.getId()));\n        for (String instanceId : instances) {\n            if (isActiveInstance(instanceId)) {\n                LOGGER.info(String.format(\"ASG %s has active instance.\", resource.getId()));\n                return true;\n            }\n        }\n        LOGGER.info(String.format(\"ASG %s has no active instance.\", resource.getId()));\n        return false;\n    }\n\n    /**\n     * Returns true if the instance is registered in Eureka/Discovery.\n     * @param instanceId the instance id\n     * @return true if the instance is active, false otherwise\n     */\n    private boolean isActiveInstance(String instanceId) {\n        Validate.notNull(instanceId);\n        LOGGER.debug(String.format(\"Checking if instance %s is active\", instanceId));\n        List<InstanceInfo> instanceInfos = discoveryClient.getInstancesById(instanceId);\n        for (InstanceInfo info : instanceInfos) {\n            InstanceStatus status = info.getStatus();\n            if (status == InstanceStatus.UP || status == InstanceStatus.STARTING) {\n                LOGGER.debug(String.format(\"Instance %s is active in Discovery.\", instanceId));\n                return true;\n            }\n        }\n        LOGGER.debug(String.format(\"Instance %s is not active in Discovery.\", instanceId));\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/DummyASGInstanceValidator.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.rule.asg;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * A dummy implementation of ASGInstanceValidator that considers every instance as active.\n */\npublic class DummyASGInstanceValidator implements ASGInstanceValidator {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(DummyASGInstanceValidator.class);\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean hasActiveInstance(Resource resource) {\n        String instanceIds = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES);\n        String maxSizeStr = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE);\n        if (StringUtils.isBlank(instanceIds)) {\n            if (maxSizeStr != null && Integer.parseInt(maxSizeStr) == 0) {\n                // The ASG is empty when it has no instance and the max size of the ASG is 0.\n                // If the max size is not 0, the ASG could probably be in the process of starting new instances.\n                LOGGER.info(String.format(\"ASG %s is empty.\", resource.getId()));\n                return false;\n            } else {\n                LOGGER.info(String.format(\"ASG %s does not have instances but the max size is %s\",\n                        resource.getId(), maxSizeStr));\n                return true;\n            }\n        }\n        String[] instances = StringUtils.split(instanceIds, \",\");\n        return instances.length > 0;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/OldEmptyASGRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.asg;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;\nimport com.netflix.simianarmy.aws.janitor.crawler.edda.EddaASGJanitorCrawler;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Date;\n\n/**\n * The rule for detecting the ASGs that 1) have old launch configurations and\n * 2) do not have any instances or all instances are inactive in Eureka.\n * 3) are not fronted with any ELBs.\n */\npublic class OldEmptyASGRule implements Rule {\n\n    private final MonkeyCalendar calendar;\n    private final int retentionDays;\n    private final int launchConfigAgeThreshold;\n    private final Integer lastChangeDaysThreshold;\n    private final ASGInstanceValidator instanceValidator;\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(OldEmptyASGRule.class);\n\n    /**\n     * Constructor.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param retentionDays\n     *            The number of days that the marked ASG is retained before being terminated\n     * @param launchConfigAgeThreshold\n     *            The number of days that the launch configuration for the ASG has been created that makes the ASG be\n     *            considered obsolete\n     * @param instanceValidator\n     *            The instance validator to check if an instance is active\n     */\n    public OldEmptyASGRule(MonkeyCalendar calendar, int launchConfigAgeThreshold,\n                           int retentionDays, ASGInstanceValidator instanceValidator) {\n        this(calendar, launchConfigAgeThreshold, null, retentionDays, instanceValidator);\n    }\n\n    /**\n     * Constructor.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param retentionDays\n     *            The number of days that the marked ASG is retained before being terminated\n     * @param launchConfigAgeThreshold\n     *            The number of days that the launch configuration for the ASG has been created that makes the ASG be\n     *            considered obsolete\n     * @param  lastChangeDaysThreshold\n     *            The number of days that the launch configuration has not been changed. An ASG is considered as a\n     *            cleanup candidate only if it has no change during the last n days. The parameter can be null.\n     * @param instanceValidator\n     *            The instance validator to check if an instance is active\n     */\n    public OldEmptyASGRule(MonkeyCalendar calendar, int launchConfigAgeThreshold, Integer lastChangeDaysThreshold,\n                           int retentionDays, ASGInstanceValidator instanceValidator) {\n        Validate.notNull(calendar);\n        Validate.isTrue(retentionDays >= 0);\n        Validate.isTrue(launchConfigAgeThreshold >= 0);\n        Validate.isTrue(lastChangeDaysThreshold == null || lastChangeDaysThreshold >= 0);\n        Validate.notNull(instanceValidator);\n        this.calendar = calendar;\n        this.retentionDays = retentionDays;\n        this.launchConfigAgeThreshold = launchConfigAgeThreshold;\n        this.lastChangeDaysThreshold = lastChangeDaysThreshold;\n        this.instanceValidator = instanceValidator;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!\"ASG\".equals(resource.getResourceType().name())) {\n            return true;\n        }\n\n        if (StringUtils.isNotEmpty(resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_ELBS))) {\n            LOGGER.info(String.format(\"ASG %s has ELBs.\", resource.getId()));\n            return true;\n        }\n\n        if (instanceValidator.hasActiveInstance(resource)) {\n            LOGGER.info(String.format(\"ASG %s has active instance.\", resource.getId()));\n            return true;\n        }\n\n        String lcName = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME);\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        if (StringUtils.isEmpty(lcName)) {\n            LOGGER.error(String.format(\"Failed to find launch configuration for ASG %s\", resource.getId()));\n            markResource(resource, now);\n            return false;\n        }\n\n        String lcCreationTime = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME);\n        if (StringUtils.isEmpty(lcCreationTime)) {\n            LOGGER.error(String.format(\"Failed to find creation time for launch configuration %s\", lcName));\n            return true;\n        }\n\n        DateTime createTime = new DateTime(Long.parseLong(lcCreationTime));\n        if (now.isBefore(createTime.plusDays(launchConfigAgeThreshold))) {\n            LOGGER.info(String.format(\"The launch configuration %s has not been created for more than %d days\",\n                    lcName, launchConfigAgeThreshold));\n            return true;\n        }\n        LOGGER.info(String.format(\"The launch configuration %s has been created for more than %d days\",\n                lcName, launchConfigAgeThreshold));\n\n        if (lastChangeDaysThreshold != null) {\n            String lastChangeTimeField = resource.getAdditionalField(EddaASGJanitorCrawler.ASG_FIELD_LAST_CHANGE_TIME);\n            if (StringUtils.isNotBlank(lastChangeTimeField)) {\n                DateTime lastChangeTime = new DateTime(Long.parseLong(lastChangeTimeField));\n                if (lastChangeTime.plusDays(lastChangeDaysThreshold).isAfter(now)) {\n                    LOGGER.info(String.format(\"ASG %s had change during the last %d days\",\n                            resource.getId(), lastChangeDaysThreshold));\n                    return true;\n                }\n            }\n        }\n\n        markResource(resource, now);\n        return false;\n    }\n\n    private void markResource(Resource resource, DateTime now) {\n        if (resource.getExpectedTerminationTime() == null) {\n            Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);\n            resource.setExpectedTerminationTime(terminationTime);\n            resource.setTerminationReason(String.format(\n                    \"Launch config older than %d days. Not in Discovery. No ELB.\",\n                    launchConfigAgeThreshold + retentionDays));\n        } else {\n            LOGGER.info(String.format(\"Resource %s is already marked as cleanup candidate.\", resource.getId()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/SuspendedASGRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.asg;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Date;\n\n/**\n * The rule for detecting the ASGs that 1) have old launch configurations and\n * 2) do not have any instances or all instances are inactive in Eureka.\n * 3) are not fronted with any ELBs.\n */\npublic class SuspendedASGRule implements Rule {\n\n    private final MonkeyCalendar calendar;\n    private final int retentionDays;\n    private final int suspensionAgeThreshold;\n    private final ASGInstanceValidator instanceValidator;\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(SuspendedASGRule.class);\n\n    /**\n     * Constructor.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param retentionDays\n     *            The number of days that the marked ASG is retained before being terminated after\n     *            being marked\n     * @param suspensionAgeThreshold\n     *            The number of days that the ASG has been suspended from ELB that makes the ASG be\n     *            considered a cleanup candidate\n     * @param instanceValidator\n     *            The instance validator to check if an instance is active\n     */\n    public SuspendedASGRule(MonkeyCalendar calendar, int suspensionAgeThreshold, int retentionDays,\n                            ASGInstanceValidator instanceValidator) {\n        Validate.notNull(calendar);\n        Validate.isTrue(retentionDays >= 0);\n        Validate.isTrue(suspensionAgeThreshold >= 0);\n        Validate.notNull(instanceValidator);\n        this.calendar = calendar;\n        this.retentionDays = retentionDays;\n        this.suspensionAgeThreshold = suspensionAgeThreshold;\n        this.instanceValidator = instanceValidator;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!\"ASG\".equals(resource.getResourceType().name())) {\n            return true;\n        }\n\n        if (instanceValidator.hasActiveInstance(resource)) {\n            return true;\n        }\n\n        String suspensionTimeStr = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME);\n        if (!StringUtils.isEmpty(suspensionTimeStr)) {\n            DateTime createTime = ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.parseDateTime(suspensionTimeStr);\n            DateTime now = new DateTime(calendar.now().getTimeInMillis());\n            if (now.isBefore(createTime.plusDays(suspensionAgeThreshold))) {\n                LOGGER.info(String.format(\"The ASG %s has not been suspended for more than %d days\",\n                        resource.getId(), suspensionAgeThreshold));\n                return true;\n            }\n            LOGGER.info(String.format(\"The ASG %s has been suspended for more than %d days\",\n                    resource.getId(), suspensionAgeThreshold));\n            if (resource.getExpectedTerminationTime() == null) {\n                Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);\n                resource.setExpectedTerminationTime(terminationTime);\n                resource.setTerminationReason(String.format(\n                        \"ASG has been disabled for more than %d days and all instances are out of service in Discovery\",\n                        suspensionAgeThreshold + retentionDays));\n            }\n            return false;\n        } else {\n            LOGGER.info(String.format(\"ASG %s is not suspended from ELB.\", resource.getId()));\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/elb/OrphanedELBRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.elb;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.apache.commons.lang.math.NumberUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Date;\n\n/**\n * The rule for checking the orphaned instances that do not belong to any ASGs and\n * launched for certain days.\n */\npublic class OrphanedELBRule implements Rule {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(OrphanedELBRule.class);\n\n    private static final String TERMINATION_REASON = \"ELB has no instances and is not referenced by any ASG\";\n    private final MonkeyCalendar calendar;\n    private final int retentionDays;\n\n    /**\n     * Constructor for OrphanedELBRule.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param retentionDays\n     *            The number of days that the marked ASG is retained before being terminated\n     */\n    public OrphanedELBRule(MonkeyCalendar calendar, int retentionDays) {\n        Validate.notNull(calendar);\n        Validate.isTrue(retentionDays >= 0);\n        this.calendar = calendar;\n        this.retentionDays = retentionDays;\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!resource.getResourceType().name().equals(\"ELB\")) {\n            return true;\n        }\n\n        String instanceCountStr = resource.getAdditionalField(\"instanceCount\");\n        String refASGCountStr = resource.getAdditionalField(\"referencedASGCount\");\n        if (StringUtils.isBlank(instanceCountStr)) {\n            LOGGER.info(String.format(\"Resource %s is missing instance count, not marked as a cleanup candidate.\", resource.getId()));\n            return true;\n        }\n        if (StringUtils.isBlank(refASGCountStr)) {\n            LOGGER.info(String.format(\"Resource %s is missing referenced ASG count, not marked as a cleanup candidate.\", resource.getId()));\n            return true;\n        }\n\n        int instanceCount = NumberUtils.toInt(instanceCountStr);\n        int refASGCount = NumberUtils.toInt(refASGCountStr);\n        if (instanceCount == 0 && refASGCount == 0) {\n            LOGGER.info(String.format(\"Resource %s is marked as cleanup candidate with 0 instances and 0 referenced ASGs (owner: %s).\", resource.getId(), resource.getOwnerEmail()));\n            markResource(resource);\n            return false;\n        } else {\n            LOGGER.info(String.format(\"Resource %s is not marked as cleanup candidate with %d instances and %d referenced ASGs.\", resource.getId(), instanceCount, refASGCount));\n            return true;\n        }\n    }\n\n    private void markResource(Resource resource) {\n        if (resource.getExpectedTerminationTime() == null) {\n            Date terminationTime = calendar.getBusinessDay(new Date(), retentionDays);\n            resource.setExpectedTerminationTime(terminationTime);\n            resource.setTerminationReason(TERMINATION_REASON);\n        } else {\n            LOGGER.info(String.format(\"Resource %s is already marked as cleanup candidate.\", resource.getId()));\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/generic/TagValueExclusionRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.generic;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * A rule for excluding resources that contain the provided tags (name and value).\n *\n * If a resource contains the tag and the appropriate value, it will be excluded from any\n * other janitor rules and will not be cleaned.\n *\n */\npublic class TagValueExclusionRule implements Rule {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(TagValueExclusionRule.class);\n    private final Map<String,String> tags;\n\n    /**\n     * Constructor for TagValueExclusionRule.\n     *\n     * @param tags\n     *            Set of tags and values to match for exclusion\n     */\n    public TagValueExclusionRule(Map<String, String> tags) {\n        this.tags = tags;\n    }\n\n    /**\n     * Constructor for TagValueExclusionRule.  Use this constructor to pass names and values as separate args.\n     * This is intended for convenience when specifying tag names/values in property files.\n     *\n     * Each tag[i] = (name[i], value[i])\n     *\n     * @param names\n     *            Set of names to match for exclusion.  Size of names must match size of values.\n     * @param values\n     *            Set of values to match for exclusion.  Size of names must match size of values.\n     */\n    public TagValueExclusionRule(String[] names, String[] values) {\n        tags = new HashMap<String,String>();\n        int i = 0;\n        for(String name : names) {\n            tags.put(name, values[i]);\n            i++;\n        }\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        for (String tagName : tags.keySet()) {\n            String resourceValue = resource.getTag(tagName);\n            if (resourceValue != null && resourceValue.equals(tags.get(tagName))) {\n                LOGGER.debug(String.format(\"The resource %s has the exclusion tag %s with value %s\", resource.getId(), tagName, resourceValue));\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/generic/UntaggedRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.generic;\n\nimport java.util.Date;\nimport java.util.Set;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.janitor.Rule;\n\n/**\n * The rule for checking the orphaned instances that do not belong to any ASGs and\n * launched for certain days.\n */\npublic class UntaggedRule implements Rule {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(UntaggedRule.class);\n\n    private static final String TERMINATION_REASON = \"This resource is missing the required tags\";\n\n    private final MonkeyCalendar calendar;\n\n    private final Set<String> tagNames;\n\n    private final int retentionDaysWithOwner;\n\n    private final int retentionDaysWithoutOwner;\n\n\n    /**\n     * Constructor for UntaggedInstanceRule.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param tagNames\n     *            Set of tags that needs to be set\n     */\n    public UntaggedRule(MonkeyCalendar calendar, Set<String> tagNames, int retentionDaysWithOwner, int retentionDaysWithoutOwner) {\n        Validate.notNull(calendar);\n        Validate.notNull(tagNames);\n        this.calendar = calendar;\n        this.tagNames = tagNames;\n        this.retentionDaysWithOwner = retentionDaysWithOwner;\n        this.retentionDaysWithoutOwner = retentionDaysWithoutOwner;\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        for (String tagName : this.tagNames) {\n            if (((AWSResource) resource).getTag(tagName) == null) {\n                String terminationReason = String.format(\" does not have the required tag %s\", tagName);\n                LOGGER.error(String.format(\"The resource %s %s\", resource.getId(), terminationReason));\n                DateTime now = new DateTime(calendar.now().getTimeInMillis());\n                if (resource.getExpectedTerminationTime() == null) {\n                    int retentionDays = retentionDaysWithoutOwner;\n                    if (resource.getOwnerEmail() != null) {\n                        retentionDays = retentionDaysWithOwner;\n                    }\n                    Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);\n                    resource.setExpectedTerminationTime(terminationTime);\n                    resource.setTerminationReason(terminationReason);\n                }\n                return false;\n            } else {\n                LOGGER.debug(String.format(\"The resource %s has the required tag %s\", resource.getId(), tagName));\n            }\n        }\n        LOGGER.info(String.format(\"The resource %s has all required tags\", resource.getId()));\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/instance/OrphanedInstanceRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.instance;\n\nimport java.util.Date;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler;\nimport com.netflix.simianarmy.janitor.Rule;\n\n/**\n * The rule for checking the orphaned instances that do not belong to any ASGs and\n * launched for certain days.\n */\npublic class OrphanedInstanceRule implements Rule {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(OrphanedInstanceRule.class);\n\n    private static final String TERMINATION_REASON = \"No ASG is associated with this instance\";\n    private static final String ASG_OR_OPSWORKS_TERMINATION_REASON = \"No ASG or OpsWorks stack is associated with this instance\";\n\n    private final MonkeyCalendar calendar;\n\n    private final int instanceAgeThreshold;\n\n    private final int retentionDaysWithOwner;\n\n    private final int retentionDaysWithoutOwner;\n    \n    private final boolean respectOpsWorksParentage;\n\n    /**\n     * Constructor for OrphanedInstanceRule.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param instanceAgeThreshold\n     *            The number of days that an instance is considered as orphaned since it is launched\n     * @param retentionDaysWithOwner\n     *            The number of days that the orphaned instance is retained before being terminated\n     *            when the instance has an owner specified\n     * @param retentionDaysWithoutOwner\n     *            The number of days that the orphaned instance is retained before being terminated\n     *            when the instance has no owner specified\n     * @param respectOpsWorksParentage\n     *            If true, don't consider members of an OpsWorks stack as orphans \n     */\n    public OrphanedInstanceRule(MonkeyCalendar calendar,\n            int instanceAgeThreshold, int retentionDaysWithOwner, int retentionDaysWithoutOwner, boolean respectOpsWorksParentage) {\n        Validate.notNull(calendar);\n        Validate.isTrue(instanceAgeThreshold >= 0);\n        Validate.isTrue(retentionDaysWithOwner >= 0);\n        Validate.isTrue(retentionDaysWithoutOwner >= 0);\n        this.calendar = calendar;\n        this.instanceAgeThreshold = instanceAgeThreshold;\n        this.retentionDaysWithOwner = retentionDaysWithOwner;\n        this.retentionDaysWithoutOwner = retentionDaysWithoutOwner;\n        this.respectOpsWorksParentage = respectOpsWorksParentage;\n    }\n    \n    public OrphanedInstanceRule(MonkeyCalendar calendar,\n            int instanceAgeThreshold, int retentionDaysWithOwner, int retentionDaysWithoutOwner) {\n        this(calendar, instanceAgeThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner, false);\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!resource.getResourceType().name().equals(\"INSTANCE\")) {\n            // The rule is supposed to only work on AWS instances. If a non-instance resource\n            // is passed to the rule, the rule simply ignores it and considers it as a valid\n            // resource not for cleanup.\n            return true;\n        }\n        String awsStatus = ((AWSResource) resource).getAWSResourceState();\n        if (!\"running\".equals(awsStatus) || \"pending\".equals(awsStatus)) {\n            return true;\n        }\n        AWSResource instanceResource = (AWSResource) resource;\n        String asgName = instanceResource.getAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME);\n        String opsworkStackName = instanceResource.getAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_OPSWORKS_STACK_NAME);\n        // If there is no ASG AND it isn't an OpsWorks stack (or OpsWorks isn't respected as a parent), we have an orphan\n        if (StringUtils.isEmpty(asgName) && (!respectOpsWorksParentage || StringUtils.isEmpty(opsworkStackName))) {\n            if (resource.getLaunchTime() == null) {\n                LOGGER.error(String.format(\"The instance %s has no launch time.\", resource.getId()));\n                return true;\n            } else {\n                DateTime launchTime = new DateTime(resource.getLaunchTime().getTime());\n                DateTime now = new DateTime(calendar.now().getTimeInMillis());\n                if (now.isBefore(launchTime.plusDays(instanceAgeThreshold))) {\n                    LOGGER.info(String.format(\"The orphaned instance %s has not launched for more than %d days\",\n                            resource.getId(), instanceAgeThreshold));\n                    return true;\n                }\n                LOGGER.info(String.format(\"The orphaned instance %s has launched for more than %d days\",\n                        resource.getId(), instanceAgeThreshold));\n                if (resource.getExpectedTerminationTime() == null) {\n                    int retentionDays = retentionDaysWithoutOwner;\n                    if (resource.getOwnerEmail() != null) {\n                        retentionDays = retentionDaysWithOwner;\n                    }\n                    Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);\n                    resource.setExpectedTerminationTime(terminationTime);\n                    resource.setTerminationReason((respectOpsWorksParentage) ? ASG_OR_OPSWORKS_TERMINATION_REASON : TERMINATION_REASON);\n                }\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/launchconfig/OldUnusedLaunchConfigRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.aws.janitor.rule.launchconfig;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.janitor.crawler.LaunchConfigJanitorCrawler;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Date;\n\n/**\n * The rule for detecting the launch configurations that\n * 1) have been created for certain days and\n * 2) are not used by any auto scaling groups.\n */\npublic class OldUnusedLaunchConfigRule implements Rule {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(OldUnusedLaunchConfigRule.class);\n\n    private static final String TERMINATION_REASON = \"Launch config is not used by any ASG\";\n\n    private final MonkeyCalendar calendar;\n\n    private final int ageThreshold;\n\n    private final int retentionDays;\n\n    /**\n     * Constructor for OrphanedInstanceRule.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param ageThreshold\n     *            The number of days that a launch configuration is considered as a cleanup candidate\n     *            since it is created\n     * @param retentionDays\n     *            The number of days that the unused launch configuration is retained before being terminated\n     */\n    public OldUnusedLaunchConfigRule(MonkeyCalendar calendar, int ageThreshold, int retentionDays) {\n        Validate.notNull(calendar);\n        Validate.isTrue(ageThreshold >= 0);\n        Validate.isTrue(retentionDays >= 0);\n        this.calendar = calendar;\n        this.ageThreshold = ageThreshold;\n        this.retentionDays = retentionDays;\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!\"LAUNCH_CONFIG\".equals(resource.getResourceType().name())) {\n            return true;\n        }\n        AWSResource lcResource = (AWSResource) resource;\n        String usedByASG = lcResource.getAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG);\n        if (StringUtils.isNotEmpty(usedByASG) && !Boolean.parseBoolean(usedByASG)) {\n            if (resource.getLaunchTime() == null) {\n                LOGGER.error(String.format(\"The launch config %s has no creation time.\", resource.getId()));\n                return true;\n            } else {\n                DateTime launchTime = new DateTime(resource.getLaunchTime().getTime());\n                DateTime now = new DateTime(calendar.now().getTimeInMillis());\n                if (now.isBefore(launchTime.plusDays(ageThreshold))) {\n                    LOGGER.info(String.format(\"The unused launch config %s has not been created for more than %d days\",\n                            resource.getId(), ageThreshold));\n                    return true;\n                }\n                LOGGER.info(String.format(\"The unused launch config %s has been created for more than %d days\",\n                        resource.getId(), ageThreshold));\n                if (resource.getExpectedTerminationTime() == null) {\n                    Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);\n                    resource.setExpectedTerminationTime(terminationTime);\n                    resource.setTerminationReason(TERMINATION_REASON);\n                }\n                return false;\n            }\n        }\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/snapshot/NoGeneratedAMIRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.rule.snapshot;\n\nimport java.util.Date;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.janitor.crawler.EBSSnapshotJanitorCrawler;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\nimport com.netflix.simianarmy.janitor.Rule;\n\n/**\n * The rule is for checking whether an EBS snapshot has any AMIs generated from it.\n * If there are no AMIs generated using the snapshot and the snapshot is created\n * for certain days, it is marked as a cleanup candidate by this rule.\n */\npublic class NoGeneratedAMIRule implements Rule {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(NoGeneratedAMIRule.class);\n\n    private String ownerEmailOverride = null;\n    \n    private static final String TERMINATION_REASON = \"No AMI is generated for this snapshot\";\n\n    private final MonkeyCalendar calendar;\n\n    private final int ageThreshold;\n\n    private final int retentionDays;\n\n    /** The date format used to print or parse the user specified termination date. **/\n    public static final DateTimeFormatter TERMINATION_DATE_FORMATTER = DateTimeFormat.forPattern(\"yyyy-MM-dd\");\n\n    /**\n     * Constructor.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param ageThreshold\n     *            The number of days that a snapshot is considered as cleanup candidate since it is created\n     * @param retentionDays\n     *            The number of days that the volume is retained before being terminated after being marked\n     *            as cleanup candidate\n     */\n    public NoGeneratedAMIRule(MonkeyCalendar calendar, int ageThreshold, int retentionDays) {\n    \tthis(calendar, ageThreshold, retentionDays, null);\n    }\n\n\t/**\n     * Constructor.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param ageThreshold\n     *            The number of days that a snapshot is considered as cleanup candidate since it is created\n     * @param retentionDays\n     *            The number of days that the volume is retained before being terminated after being marked\n     *            as cleanup candidate\n     * @param ownerEmailOverride\n     *            If null, send notifications to the resource owner.\n     *            If not null, send notifications to the provided owner email address instead of the resource owner.\n     */\n    public NoGeneratedAMIRule(MonkeyCalendar calendar, int ageThreshold, int retentionDays, String ownerEmailOverride) {\n        Validate.notNull(calendar);\n        Validate.isTrue(ageThreshold >= 0);\n        Validate.isTrue(retentionDays >= 0);\n        this.calendar = calendar;\n        this.ageThreshold = ageThreshold;\n        this.retentionDays = retentionDays;\n        this.ownerEmailOverride = ownerEmailOverride;\n    }\n    \n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!resource.getResourceType().name().equals(\"EBS_SNAPSHOT\")) {\n            return true;\n        }\n        if (!\"completed\".equals(((AWSResource) resource).getAWSResourceState())) {\n            return true;\n        }\n        String janitorTag = resource.getTag(JanitorMonkey.JANITOR_TAG);\n        if (janitorTag != null) {\n            if (\"donotmark\".equals(janitorTag)) {\n                LOGGER.info(String.format(\"The snapshot %s is tagged as not handled by Janitor\",\n                        resource.getId()));\n                return true;\n            }\n            try {\n                // Owners can tag the volume with a termination date in the \"janitor\" tag.\n                Date userSpecifiedDate = new Date(TERMINATION_DATE_FORMATTER.parseDateTime(janitorTag).getMillis());\n                resource.setExpectedTerminationTime(userSpecifiedDate);\n                resource.setTerminationReason(String.format(\"User specified termination date %s\", janitorTag));\n                if (ownerEmailOverride != null) {\n                \tresource.setOwnerEmail(ownerEmailOverride);\n                }\n                return false;\n            } catch (Exception e) {\n                LOGGER.error(String.format(\"The janitor tag is not a user specified date: %s\", janitorTag));\n            }\n        }\n\n        if (hasGeneratedImage(resource)) {\n            return true;\n        }\n\n        if (resource.getLaunchTime() == null) {\n            LOGGER.error(String.format(\"Snapshot %s does not have a creation time.\", resource.getId()));\n            return true;\n        }\n        DateTime launchTime = new DateTime(resource.getLaunchTime().getTime());\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        if (launchTime.plusDays(ageThreshold).isBefore(now)) {\n            if (ownerEmailOverride != null) {\n            \tresource.setOwnerEmail(ownerEmailOverride);\n            }\n            if (resource.getExpectedTerminationTime() == null) {\n                Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);\n                resource.setExpectedTerminationTime(terminationTime);\n                resource.setTerminationReason(TERMINATION_REASON);\n                LOGGER.info(String.format(\n                        \"Snapshot %s is marked to be cleaned at %s as there is no AMI generated using it\",\n                        resource.getId(), resource.getExpectedTerminationTime()));\n            } else {\n                LOGGER.info(String.format(\"Resource %s is already marked.\", resource.getId()));\n            }\n            return false;\n        }\n        return true;\n\n    }\n\n    /**\n     * Gets the AMI created using the snapshot. This method can be overridden by subclasses\n     * if they use a different way to check this.\n     * @param resource the snapshot resource\n     * @return true if there are AMIs that are created using the snapshot, false otherwise\n     */\n    protected boolean hasGeneratedImage(Resource resource) {\n        return StringUtils.isNotEmpty(resource.getAdditionalField(EBSSnapshotJanitorCrawler.SNAPSHOT_FIELD_AMIS));\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/volume/DeleteOnTerminationRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.rule.volume;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.janitor.crawler.edda.EddaEBSVolumeJanitorCrawler;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Date;\n\n/**\n * The rule is for checking whether an EBS volume is not attached to any instance and had the\n * DeleteOnTermination flag set in the previous attachment. This is an error case that AWS didn't\n * handle. The volume should have been deleted as soon as it was detached.\n *\n * NOTE: since the information came from the history, the rule will work only if Edda is enabled\n * for Janitor Monkey.\n */\npublic class DeleteOnTerminationRule implements Rule {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(DeleteOnTerminationRule.class);\n\n    private final MonkeyCalendar calendar;\n\n    private final int retentionDays;\n\n    /** The date format used to print or parse the user specified termination date. **/\n    private static final DateTimeFormatter TERMINATION_DATE_FORMATTER = DateTimeFormat.forPattern(\"yyyy-MM-dd\");\n\n    /**\n     * The termination reason for the DeleteOnTerminationRule.\n     */\n    public static final String TERMINATION_REASON = \"Not attached and DeleteOnTerminate flag was set\";\n\n    /**\n     * Constructor.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param retentionDays\n     *            The number of days that the volume is retained before being terminated after being marked\n     *            as cleanup candidate\n     */\n    public DeleteOnTerminationRule(MonkeyCalendar calendar, int retentionDays) {\n        Validate.notNull(calendar);\n        Validate.isTrue(retentionDays >= 0);\n        this.calendar = calendar;\n        this.retentionDays = retentionDays;\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!resource.getResourceType().name().equals(\"EBS_VOLUME\")) {\n            return true;\n        }\n\n        // The state of the volume being \"available\" means that it is not attached to any instance.\n        if (!\"available\".equals(((AWSResource) resource).getAWSResourceState())) {\n            return true;\n        }\n        String janitorTag = resource.getTag(JanitorMonkey.JANITOR_TAG);\n        if (janitorTag != null) {\n            if (\"donotmark\".equals(janitorTag)) {\n                LOGGER.info(String.format(\"The volume %s is tagged as not handled by Janitor\",\n                        resource.getId()));\n                return true;\n            }\n            try {\n                // Owners can tag the volume with a termination date in the \"janitor\" tag.\n                Date userSpecifiedDate = new Date(\n                        TERMINATION_DATE_FORMATTER.parseDateTime(janitorTag).getMillis());\n                resource.setExpectedTerminationTime(userSpecifiedDate);\n                resource.setTerminationReason(String.format(\"User specified termination date %s\", janitorTag));\n                return false;\n            } catch (Exception e) {\n                LOGGER.error(String.format(\"The janitor tag is not a user specified date: %s\", janitorTag));\n            }\n        }\n\n        if (\"true\".equals(resource.getAdditionalField(EddaEBSVolumeJanitorCrawler.DELETE_ON_TERMINATION))) {\n            if (resource.getExpectedTerminationTime() == null) {\n                Date terminationTime = calendar.getBusinessDay(calendar.now().getTime(), retentionDays);\n                resource.setExpectedTerminationTime(terminationTime);\n                resource.setTerminationReason(TERMINATION_REASON);\n                LOGGER.info(String.format(\n                        \"Volume %s is marked to be cleaned at %s as it is detached and DeleteOnTermination was set\",\n                        resource.getId(), resource.getExpectedTerminationTime()));\n            } else {\n                LOGGER.info(String.format(\"Resource %s is already marked.\", resource.getId()));\n            }\n            return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/aws/janitor/rule/volume/OldDetachedVolumeRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws.janitor.rule.volume;\n\nimport java.util.Date;\nimport java.util.Map;\n\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\nimport com.netflix.simianarmy.janitor.Rule;\n\n/**\n * The rule is for checking whether an EBS volume is detached for more than\n * certain days. The rule mostly relies on tags on the volume to decide if\n * the volume should be marked.\n */\npublic class OldDetachedVolumeRule implements Rule {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(OldDetachedVolumeRule.class);\n\n    private final MonkeyCalendar calendar;\n\n    private final int detachDaysThreshold;\n\n    private final int retentionDays;\n\n    /** The date format used to print or parse the user specified termination date. **/\n    public static final DateTimeFormatter TERMINATION_DATE_FORMATTER = DateTimeFormat.forPattern(\"yyyy-MM-dd\");\n\n\n    /**\n     * Constructor.\n     *\n     * @param calendar\n     *            The calendar used to calculate the termination time\n     * @param detachDaysThreshold\n     *            The number of days that a volume is considered as cleanup candidate since it is detached\n     * @param retentionDays\n     *            The number of days that the volume is retained before being terminated after being marked\n     *            as cleanup candidate\n     */\n    public OldDetachedVolumeRule(MonkeyCalendar calendar, int detachDaysThreshold, int retentionDays) {\n        Validate.notNull(calendar);\n        Validate.isTrue(detachDaysThreshold >= 0);\n        Validate.isTrue(retentionDays >= 0);\n        this.calendar = calendar;\n        this.detachDaysThreshold = detachDaysThreshold;\n        this.retentionDays = retentionDays;\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        Validate.notNull(resource);\n        if (!resource.getResourceType().name().equals(\"EBS_VOLUME\")) {\n            return true;\n        }\n        if (!\"available\".equals(((AWSResource) resource).getAWSResourceState())) {\n            return true;\n        }\n        String janitorTag = resource.getTag(JanitorMonkey.JANITOR_TAG);\n        if (janitorTag != null) {\n            if (\"donotmark\".equals(janitorTag)) {\n                LOGGER.info(String.format(\"The volume %s is tagged as not handled by Janitor\",\n                        resource.getId()));\n                return true;\n            }\n            try {\n                // Owners can tag the volume with a termination date in the \"janitor\" tag.\n                Date userSpecifiedDate = new Date(\n                        TERMINATION_DATE_FORMATTER.parseDateTime(janitorTag).getMillis());\n                resource.setExpectedTerminationTime(userSpecifiedDate);\n                resource.setTerminationReason(String.format(\"User specified termination date %s\", janitorTag));\n                return false;\n            } catch (Exception e) {\n                LOGGER.error(String.format(\"The janitor tag is not a user specified date: %s\", janitorTag));\n            }\n        }\n\n        String janitorMetaTag = resource.getTag(JanitorMonkey.JANITOR_META_TAG);\n        if (janitorMetaTag == null) {\n            LOGGER.info(String.format(\"Volume %s is not tagged with the Janitor meta information, ignore.\",\n                    resource.getId()));\n            return true;\n        }\n\n        Map<String, String> metadata = VolumeTaggingMonkey.parseJanitorMetaTag(janitorMetaTag);\n        String detachTimeTag = metadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY);\n        if (detachTimeTag == null) {\n            return true;\n        }\n        DateTime detachTime = null;\n        try {\n            detachTime = AWSResource.DATE_FORMATTER.parseDateTime(detachTimeTag);\n        } catch (Exception e) {\n            LOGGER.error(String.format(\"Detach time in the JANITOR_META tag of %s is not in the valid format: %s\",\n                    resource.getId(), detachTime));\n            return true;\n        }\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        if (detachTime != null && detachTime.plusDays(detachDaysThreshold).isBefore(now)) {\n            if (resource.getExpectedTerminationTime() == null) {\n                Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays);\n                resource.setExpectedTerminationTime(terminationTime);\n                resource.setTerminationReason(String.format(\"Volume not attached for %d days\",\n                        detachDaysThreshold + retentionDays));\n                LOGGER.info(String.format(\n                        \"Volume %s is marked to be cleaned at %s as it is detached for more than %d days\",\n                        resource.getId(), resource.getExpectedTerminationTime(), detachDaysThreshold));\n            } else {\n                LOGGER.info(String.format(\"Resource %s is already marked.\", resource.getId()));\n            }\n            return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/BasicCalendar.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic;\n\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Set;\nimport java.util.TimeZone;\nimport java.util.TreeSet;\n\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\n/**\n * The Class BasicCalendar.\n */\npublic class BasicCalendar implements MonkeyCalendar {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicCalendar.class);\n\n    /** The open hour. */\n    private final int openHour;\n\n    /** The close hour. */\n    private final int closeHour;\n\n    /** The tz. */\n    private final TimeZone tz;\n\n    /** The holidays. */\n    protected final Set<Integer> holidays = new TreeSet<Integer>();\n\n    /** The cfg. */\n    private MonkeyConfiguration cfg;\n\n    /**\n     * Instantiates a new basic calendar.\n     *\n     * @param cfg\n     *            the monkey configuration\n     */\n    public BasicCalendar(MonkeyConfiguration cfg) {\n        this.cfg = cfg;\n        openHour = (int) cfg.getNumOrElse(\"simianarmy.calendar.openHour\", 9);\n        closeHour = (int) cfg.getNumOrElse(\"simianarmy.calendar.closeHour\", 15);\n        tz = TimeZone.getTimeZone(cfg.getStrOrElse(\"simianarmy.calendar.timezone\", \"America/Los_Angeles\"));\n    }\n\n    /**\n     * Instantiates a new basic calendar.\n     *\n     * @param open\n     *            the open hour\n     * @param close\n     *            the close hour\n     * @param timezone\n     *            the timezone\n     */\n    public BasicCalendar(int open, int close, TimeZone timezone) {\n        openHour = open;\n        closeHour = close;\n        tz = timezone;\n    }\n\n    /**\n     * Instantiates a new basic calendar.\n     *\n     * @param open\n     *            the open hour\n     * @param close\n     *            the close hour\n     * @param timezone\n     *            the timezone\n     */\n    public BasicCalendar(MonkeyConfiguration cfg, int open, int close, TimeZone timezone) {\n        this.cfg = cfg;\n        openHour = open;\n        closeHour = close;\n        tz = timezone;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public int openHour() {\n        return openHour;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public int closeHour() {\n        return closeHour;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Calendar now() {\n        return Calendar.getInstance(tz);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean isMonkeyTime(Monkey monkey) {\n        if (cfg != null && cfg.getStr(\"simianarmy.calendar.isMonkeyTime\") != null) {\n        \tboolean monkeyTime = cfg.getBool(\"simianarmy.calendar.isMonkeyTime\");\n        \tif (monkeyTime) {\n        \t\tLOGGER.debug(\"isMonkeyTime: Found property 'simianarmy.calendar.isMonkeyTime': \" + monkeyTime + \". Time for monkey.\");\n        \t\treturn monkeyTime;\n        \t} else {\n        \t\tLOGGER.debug(\"isMonkeyTime: Found property 'simianarmy.calendar.isMonkeyTime': \" + monkeyTime + \". Continuing regular calendar check for monkey time.\");\n        \t}\n        }\n\n        Calendar now = now();\n        int dow = now.get(Calendar.DAY_OF_WEEK);\n        if (dow == Calendar.SATURDAY || dow == Calendar.SUNDAY) {\n        \tLOGGER.debug(\"isMonkeyTime: Happy Weekend! Not time for monkey.\");\n            return false;\n        }\n\n        int hour = now.get(Calendar.HOUR_OF_DAY);\n        if (hour < openHour || hour > closeHour) {\n        \tLOGGER.debug(\"isMonkeyTime: Not inside open hours. Not time for monkey.\");\n            return false;\n        }\n\n        if (isHoliday(now)) {\n        \tLOGGER.debug(\"isMonkeyTime: Happy Holiday! Not time for monkey.\");\n            return false;\n        }\n\n    \tLOGGER.debug(\"isMonkeyTime: Time for monkey.\");\n        return true;\n    }\n\n    /**\n     * Checks if is holiday.\n     *\n     * @param now\n     *            the current time\n     * @return true, if is holiday\n     */\n    protected boolean isHoliday(Calendar now) {\n        if (!holidays.contains(now.get(Calendar.YEAR))) {\n            loadHolidays(now.get(Calendar.YEAR));\n        }\n        return holidays.contains(now.get(Calendar.DAY_OF_YEAR));\n    }\n\n    /**\n     * Load holidays.\n     *\n     * @param year\n     *            the year\n     */\n    protected void loadHolidays(int year) {\n        holidays.clear();\n        // these aren't all strictly holidays, but days when engineers will likely\n        // not be in the office to respond to rampaging monkeys\n\n        // new years, or closest work day\n        holidays.add(workDayInYear(year, Calendar.JANUARY, 1));\n\n        // 3rd monday == MLK Day\n        holidays.add(dayOfYear(year, Calendar.JANUARY, Calendar.MONDAY, 3));\n\n        // 3rd monday == Presidents Day\n        holidays.add(dayOfYear(year, Calendar.FEBRUARY, Calendar.MONDAY, 3));\n\n        // last monday == Memorial Day\n        holidays.add(dayOfYear(year, Calendar.MAY, Calendar.MONDAY, -1));\n\n        // 4th of July, or closest work day\n        holidays.add(workDayInYear(year, Calendar.JULY, 4));\n\n        // first monday == Labor Day\n        holidays.add(dayOfYear(year, Calendar.SEPTEMBER, Calendar.MONDAY, 1));\n\n        // second monday == Columbus Day\n        holidays.add(dayOfYear(year, Calendar.OCTOBER, Calendar.MONDAY, 2));\n\n        // veterans day, Nov 11th, or closest work day\n        holidays.add(workDayInYear(year, Calendar.NOVEMBER, 11));\n\n        // 4th thursday == Thanksgiving\n        holidays.add(dayOfYear(year, Calendar.NOVEMBER, Calendar.THURSDAY, 4));\n\n        // 4th friday == \"black friday\", monkey goes shopping!\n        holidays.add(dayOfYear(year, Calendar.NOVEMBER, Calendar.FRIDAY, 4));\n\n        // christmas eve\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 24));\n        // christmas day\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 25));\n        // day after christmas\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 26));\n\n        // new years eve\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 31));\n\n        // mark the holiday set with the year, so on Jan 1 it will automatically\n        // recalculate the holidays for next year\n        holidays.add(year);\n    }\n\n    /**\n     * Day of year.\n     *\n     * @param year\n     *            the year\n     * @param month\n     *            the month\n     * @param day\n     *            the day\n     * @return the day of the year\n     */\n    protected int dayOfYear(int year, int month, int day) {\n        Calendar holiday = now();\n        holiday.set(Calendar.YEAR, year);\n        holiday.set(Calendar.MONTH, month);\n        holiday.set(Calendar.DAY_OF_MONTH, day);\n        return holiday.get(Calendar.DAY_OF_YEAR);\n    }\n\n    /**\n     * Day of year.\n     *\n     * @param year\n     *            the year\n     * @param month\n     *            the month\n     * @param dayOfWeek\n     *            the day of week\n     * @param weekInMonth\n     *            the week in month\n     * @return the day of the year\n     */\n    protected int dayOfYear(int year, int month, int dayOfWeek, int weekInMonth) {\n        Calendar holiday = now();\n        holiday.set(Calendar.YEAR, year);\n        holiday.set(Calendar.MONTH, month);\n        holiday.set(Calendar.DAY_OF_WEEK, dayOfWeek);\n        holiday.set(Calendar.DAY_OF_WEEK_IN_MONTH, weekInMonth);\n        return holiday.get(Calendar.DAY_OF_YEAR);\n    }\n\n    /**\n     * Work day in year.\n     *\n     * @param year\n     *            the year\n     * @param month\n     *            the month\n     * @param day\n     *            the day\n     * @return the day of the year adjusted to the closest workday\n     */\n    protected int workDayInYear(int year, int month, int day) {\n        Calendar holiday = now();\n        holiday.set(Calendar.YEAR, year);\n        holiday.set(Calendar.MONTH, month);\n        holiday.set(Calendar.DAY_OF_MONTH, day);\n        int doy = holiday.get(Calendar.DAY_OF_YEAR);\n        int dow = holiday.get(Calendar.DAY_OF_WEEK);\n\n        if (dow == Calendar.SATURDAY) {\n            return doy - 1; // FRIDAY\n        }\n\n        if (dow == Calendar.SUNDAY) {\n            return doy + 1; // MONDAY\n        }\n\n        return doy;\n    }\n\n    @Override\n    public Date getBusinessDay(Date date, int n) {\n        Validate.isTrue(n >= 0);\n        Calendar calendar = now();\n        calendar.setTime(date);\n        while (isHoliday(calendar) || isWeekend(calendar) || n-- > 0) {\n            calendar.add(Calendar.DATE, 1);\n        }\n        return calendar.getTime();\n    }\n\n    private boolean isWeekend(Calendar calendar) {\n        int dow = calendar.get(Calendar.DAY_OF_WEEK);\n        return dow == Calendar.SATURDAY || dow == Calendar.SUNDAY;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/BasicChaosMonkeyContext.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic;\n\nimport com.amazonaws.regions.Region;\nimport com.amazonaws.regions.Regions;\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.basic.chaos.BasicChaosEmailNotifier;\nimport com.netflix.simianarmy.basic.chaos.BasicChaosInstanceSelector;\nimport com.netflix.simianarmy.chaos.ChaosCrawler;\nimport com.netflix.simianarmy.chaos.ChaosEmailNotifier;\nimport com.netflix.simianarmy.chaos.ChaosInstanceSelector;\nimport com.netflix.simianarmy.chaos.ChaosMonkey;\nimport com.netflix.simianarmy.client.aws.chaos.ASGChaosCrawler;\nimport com.netflix.simianarmy.client.aws.chaos.FilteringChaosCrawler;\nimport com.netflix.simianarmy.client.aws.chaos.TagPredicate;\n\n/**\n * The Class BasicContext. This provide the basic context needed for the Chaos Monkey to run. It will configure\n * the Chaos Monkey based on a simianarmy.properties file and chaos.properties. The properties file can be\n * overridden with -Dsimianarmy.properties=/path/to/my.properties\n */\npublic class BasicChaosMonkeyContext extends BasicSimianArmyContext implements ChaosMonkey.Context {\n\n    /** The crawler. */\n    private ChaosCrawler crawler;\n\n    /** The selector. */\n    private ChaosInstanceSelector selector;\n\n    /** The chaos email notifier. */\n    private ChaosEmailNotifier chaosEmailNotifier;\n\n    /**\n     * Instantiates a new basic context.\n     */\n    public BasicChaosMonkeyContext() {\n        super(\"simianarmy.properties\", \"client.properties\", \"chaos.properties\");\n        MonkeyConfiguration cfg = configuration();\n        String tagKey = cfg.getStrOrElse(\"simianarmy.chaos.ASGtag.key\", \"\");\n        String tagValue = cfg.getStrOrElse(\"simianarmy.chaos.ASGtag.value\", \"\");\n\n        ASGChaosCrawler chaosCrawler = new ASGChaosCrawler(awsClient());\n        setChaosCrawler(tagKey.isEmpty() ? chaosCrawler : new FilteringChaosCrawler(chaosCrawler, new TagPredicate(tagKey, tagValue)));\n        setChaosInstanceSelector(new BasicChaosInstanceSelector());\n        AmazonSimpleEmailServiceClient sesClient = new AmazonSimpleEmailServiceClient(awsClientConfig);\n        if (configuration().getStr(\"simianarmy.aws.email.region\") != null) {\n           sesClient.setRegion(Region.getRegion(Regions.fromName(configuration().getStr(\"simianarmy.aws.email.region\"))));\n        }\n        setChaosEmailNotifier(new BasicChaosEmailNotifier(cfg, sesClient, null));\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public ChaosCrawler chaosCrawler() {\n        return crawler;\n    }\n\n    /**\n     * Sets the chaos crawler.\n     *\n     * @param chaosCrawler\n     *            the new chaos crawler\n     */\n    protected void setChaosCrawler(ChaosCrawler chaosCrawler) {\n        this.crawler = chaosCrawler;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public ChaosInstanceSelector chaosInstanceSelector() {\n        return selector;\n    }\n\n    /**\n     * Sets the chaos instance selector.\n     *\n     * @param chaosInstanceSelector\n     *            the new chaos instance selector\n     */\n    protected void setChaosInstanceSelector(ChaosInstanceSelector chaosInstanceSelector) {\n        this.selector = chaosInstanceSelector;\n    }\n\n    @Override\n    public ChaosEmailNotifier chaosEmailNotifier() {\n        return chaosEmailNotifier;\n    }\n\n    /**\n     * Sets the chaos email notifier.\n     *\n     * @param notifier\n     *            the chaos email notifier\n     */\n    protected void setChaosEmailNotifier(ChaosEmailNotifier notifier) {\n        this.chaosEmailNotifier = notifier;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/BasicConfiguration.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport java.util.Properties;\n\n/**\n * The Class BasicConfiguration.\n */\npublic class BasicConfiguration implements MonkeyConfiguration {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicConfiguration.class);\n\n    /** The properties. */\n    private Properties props;\n\n    /**\n     * Instantiates a new basic configuration.\n     * @param props\n     *            the properties\n     */\n    public BasicConfiguration(Properties props) {\n        this.props = props;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean getBool(String property) {\n        return getBoolOrElse(property, false);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean getBoolOrElse(String property, boolean dflt) {\n        String val = props.getProperty(property);\n        if (val == null) {\n            return dflt;\n        }\n        val = val.trim();\n        return Boolean.parseBoolean(val);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public double getNumOrElse(String property, double dflt) {\n        String val = props.getProperty(property);\n        double result = dflt;\n        if (val != null && !val.isEmpty()) {\n            try {\n                result = Double.parseDouble(val);\n            } catch (NumberFormatException e) {\n                LOGGER.error(\"failed to parse property: \" + property + \"; returning default value: \" + dflt, e);\n            }\n        }\n        return result;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getStr(String property) {\n        return getStrOrElse(property, null);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getStrOrElse(String property, String dflt) {\n        String val = props.getProperty(property);\n        return val == null ? dflt : val;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void reload() {\n        // BasicConfiguration is based on static properties, so reload is a no-op\n    }\n\n    @Override\n    public void reload(String groupName) {\n        // BasicConfiguration is based on static properties, so reload is a no-op\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/BasicMonkeyServer.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Properties;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServlet;\n\nimport com.netflix.simianarmy.basic.conformity.BasicConformityMonkey;\nimport com.netflix.simianarmy.basic.conformity.BasicConformityMonkeyContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.MonkeyRunner;\nimport com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey;\nimport com.netflix.simianarmy.basic.janitor.BasicJanitorMonkey;\nimport com.netflix.simianarmy.basic.janitor.BasicJanitorMonkeyContext;\nimport com.netflix.simianarmy.basic.janitor.BasicVolumeTaggingMonkeyContext;\n\n/**\n * Will periodically run the configured monkeys.\n */\n@SuppressWarnings(\"serial\")\npublic class BasicMonkeyServer extends HttpServlet {\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicMonkeyServer.class);\n\n    private static final MonkeyRunner RUNNER = MonkeyRunner.getInstance();\n\n    /**\n     * Add the monkeys that will be run.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public void addMonkeysToRun() {\n        LOGGER.info(\"Adding Chaos Monkey.\");\n        RUNNER.replaceMonkey(this.chaosClass, this.chaosContextClass);\n        LOGGER.info(\"Adding Volume Tagging Monkey.\");\n        RUNNER.replaceMonkey(VolumeTaggingMonkey.class, BasicVolumeTaggingMonkeyContext.class);\n        LOGGER.info(\"Adding Janitor Monkey.\");\n        RUNNER.replaceMonkey(BasicJanitorMonkey.class, BasicJanitorMonkeyContext.class);\n        LOGGER.info(\"Adding Conformity Monkey.\");\n        RUNNER.replaceMonkey(BasicConformityMonkey.class, BasicConformityMonkeyContext.class);\n    }\n\n    /**\n     * make the class of the client object configurable.\n     */\n    @SuppressWarnings(\"rawtypes\")\n    private Class chaosContextClass = com.netflix.simianarmy.basic.BasicChaosMonkeyContext.class;\n\n    /**\n     * make the class of the chaos object configurable.\n     */\n    @SuppressWarnings(\"rawtypes\")\n    private Class chaosClass = com.netflix.simianarmy.basic.chaos.BasicChaosMonkey.class;\n\n    @Override\n    public void init() throws ServletException {\n        super.init();\n        configureClient();\n        addMonkeysToRun();\n        RUNNER.start();\n    }\n\n    /**\n     * Loads the client that is configured.\n     * @throws ServletException\n     *             if the configured client cannot be loaded properly\n     */\n    @SuppressWarnings(\"rawtypes\")\n    private void configureClient() throws ServletException {\n        Properties clientConfig = loadClientConfigProperties();\n\n        Class newContextClass = loadClientClass(clientConfig, \"simianarmy.client.context.class\");\n        this.chaosContextClass = (newContextClass == null ? this.chaosContextClass : newContextClass);\n\n        Class newChaosClass = loadClientClass(clientConfig, \"simianarmy.client.chaos.class\");\n        this.chaosClass = (newChaosClass == null ? this.chaosClass : newChaosClass);\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    private Class loadClientClass(Properties clientConfig, String key) throws ServletException {\n        ClassLoader classLoader = BasicMonkeyServer.class.getClassLoader();\n        try {\n            String clientClassName = clientConfig.getProperty(key);\n            if (clientClassName == null || clientClassName.isEmpty()) {\n                LOGGER.info(\"using standard client for \" + key);\n                return null;\n            }\n        Class newClass = classLoader.loadClass(clientClassName);\n            LOGGER.info(\"using \" + key + \" loaded \" + newClass.getCanonicalName());\n            return newClass;\n        } catch (ClassNotFoundException e) {\n            throw new ServletException(\"Could not load \" + key, e);\n        }\n    }\n\n    /**\n     * Load the client config properties file.\n     *\n     * @return Properties The contents of the client config file\n     * @throws ServletException\n     *             if the file cannot be read\n     */\n    private Properties loadClientConfigProperties() throws ServletException {\n        String propertyFileName = \"client.properties\";\n        String clientConfigFileName = System.getProperty(propertyFileName, \"/\" + propertyFileName);\n        LOGGER.info(\"using client properties \" + clientConfigFileName);\n\n        InputStream input = null;\n        Properties p = new Properties();\n        try {\n            try {\n                input = BasicMonkeyServer.class.getResourceAsStream(clientConfigFileName);\n                p.load(input);\n                return p;\n            } finally {\n                if (input != null) {\n                    input.close();\n                }\n            }\n        } catch (IOException e) {\n            throw new ServletException(\"Could not load \" + clientConfigFileName, e);\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public void destroy() {\n        RUNNER.stop();\n        LOGGER.info(\"Stopping Chaos Monkey.\");\n        RUNNER.removeMonkey(this.chaosClass);\n        LOGGER.info(\"Stopping Volume Tagging Monkey.\");\n        RUNNER.removeMonkey(VolumeTaggingMonkey.class);\n        LOGGER.info(\"Stopping Janitor Monkey.\");\n        RUNNER.removeMonkey(BasicJanitorMonkey.class);\n        LOGGER.info(\"Stopping Conformity Monkey.\");\n        RUNNER.removeMonkey(BasicConformityMonkey.class);\n        super.destroy();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/BasicRecorderEvent.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic;\n\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.MonkeyRecorder;\nimport com.netflix.simianarmy.MonkeyType;\n\n/**\n * The Class BasicRecorderEvent.\n */\npublic class BasicRecorderEvent implements MonkeyRecorder.Event {\n\n    /** The monkey type. */\n    private MonkeyType monkeyType;\n\n    /** The event type. */\n    private EventType eventType;\n\n    /** The event id. */\n    private String id;\n\n    /** The event region. */\n    private String region;\n\n    /** The fields. */\n    private Map<String, String> fields = new HashMap<String, String>();\n\n    /** The event time. */\n    private Date date;\n\n    /**\n     * Instantiates a new basic recorder event.\n     *\n     * @param monkeyType\n     *            the monkey type\n     * @param eventType\n     *            the event type\n     * @param region\n     *            the region event occurred in\n     * @param id\n     *            the event id\n     */\n    public BasicRecorderEvent(MonkeyType monkeyType, EventType eventType, String region, String id) {\n        this.monkeyType = monkeyType;\n        this.eventType = eventType;\n        this.id = id;\n        this.region = region;\n        this.date = new Date();\n    }\n\n    /**\n     * Instantiates a new basic recorder event.\n     *\n     * @param monkeyType\n     *            the monkey type\n     * @param eventType\n     *            the event type\n     * @param region\n     *            the region event occurred in\n     * @param id\n     *            the event id\n     * @param time\n     *            the event time\n     */\n    public BasicRecorderEvent(MonkeyType monkeyType, EventType eventType, String region, String id, long time) {\n        this.monkeyType = monkeyType;\n        this.eventType = eventType;\n        this.id = id;\n        this.region = region;\n        this.date = new Date(time);\n    }\n\n    /** {@inheritDoc} */\n    public String id() {\n        return id;\n    }\n\n    /** {@inheritDoc} */\n    public String region() {\n        return region;\n    }\n\n    /** {@inheritDoc} */\n    public Date eventTime() {\n        return new Date(date.getTime());\n    }\n\n    /** {@inheritDoc} */\n    public MonkeyType monkeyType() {\n        return monkeyType;\n    }\n\n    /** {@inheritDoc} */\n    public EventType eventType() {\n        return eventType;\n    }\n\n    /** {@inheritDoc} */\n    public Map<String, String> fields() {\n        return Collections.unmodifiableMap(fields);\n    }\n\n    /** {@inheritDoc} */\n    public String field(String name) {\n        return fields.get(name);\n    }\n\n    /**\n     * Adds the fields.\n     *\n     * @param toAdd\n     *            the fields to set\n     * @return <b>this</b> so you can chain many addFields calls together\n     */\n    public MonkeyRecorder.Event addFields(Map<String, String> toAdd) {\n        fields.putAll(toAdd);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    public MonkeyRecorder.Event addField(String name, String value) {\n        fields.put(name, value);\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/BasicScheduler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic;\n\nimport java.util.Calendar;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.TimeUnit;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.MonkeyScheduler;\n\n/**\n * The Class BasicScheduler.\n */\npublic class BasicScheduler implements MonkeyScheduler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicScheduler.class);\n\n    /** The futures. */\n    private HashMap<String, ScheduledFuture<?>> futures = new HashMap<String, ScheduledFuture<?>>();\n\n    /** The scheduler. */\n    private final ScheduledExecutorService scheduler;\n\n    /** the frequency. */\n    private int frequency = 1;\n\n    /** the frequencyUnit. */\n    private TimeUnit frequencyUnit = TimeUnit.HOURS;\n\n    /**\n     * Instantiates a new basic scheduler.\n     */\n    public BasicScheduler() {\n        scheduler = Executors.newScheduledThreadPool(1);\n    }\n\n    /**\n     * Instantiates a new basic scheduler.\n     *\n     * @param freq\n     *            the frequency to run on\n     * @param freqUnit\n     *            the unit for the freq argument\n     * @param concurrent\n     *            the concurrent number of threads\n     */\n    public BasicScheduler(int freq, TimeUnit freqUnit, int concurrent) {\n        frequency = freq;\n        frequencyUnit = freqUnit;\n        scheduler = Executors.newScheduledThreadPool(concurrent);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public int frequency() {\n        return frequency;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public TimeUnit frequencyUnit() {\n        return frequencyUnit;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void start(Monkey monkey, Runnable command) {\n        long cycle = TimeUnit.MILLISECONDS.convert(frequency(), frequencyUnit());\n\n        // go back 1 cycle to see if we have any events\n        Calendar cal = Calendar.getInstance();\n        cal.add(Calendar.MILLISECOND, (int) (-1 * cycle));\n\n        Date then = cal.getTime();\n        List<Event> events = monkey.context().recorder()\n                .findEvents(monkey.type(), Collections.<String, String>emptyMap(), then);\n        if (events.isEmpty()) {\n            // no events so just run now\n            futures.put(monkey.type().name(),\n                    scheduler.scheduleWithFixedDelay(command, 0, frequency(), frequencyUnit()));\n        } else {\n            // we have events, so set the start time to the time left in what would have been the last cycle\n            Date eventTime = events.get(0).eventTime();\n            Date now = new Date();\n            long init = cycle - (now.getTime() - eventTime.getTime());\n            LOGGER.info(\"Detected previous events within cycle, setting \" + monkey.type().name() + \" start to \"\n                    + new Date(now.getTime() + init));\n            futures.put(monkey.type().name(),\n                    scheduler.scheduleWithFixedDelay(command, init, cycle, TimeUnit.MILLISECONDS));\n        }\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void stop(Monkey monkey) {\n        if (futures.containsKey(monkey.type().name())) {\n            futures.remove(monkey.type().name()).cancel(true);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/BasicSimianArmyContext.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic;\n\nimport com.amazonaws.ClientConfiguration;\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.auth.DefaultAWSCredentialsProviderChain;\nimport com.amazonaws.regions.Region;\nimport com.amazonaws.regions.Regions;\nimport com.netflix.simianarmy.CloudClient;\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.MonkeyRecorder;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.MonkeyScheduler;\nimport com.netflix.simianarmy.aws.RDSRecorder;\nimport com.netflix.simianarmy.aws.STSAssumeRoleSessionCredentialsProvider;\nimport com.netflix.simianarmy.aws.SimpleDBRecorder;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.InputStream;\nimport java.lang.reflect.Constructor;\nimport java.util.LinkedList;\nimport java.util.Map.Entry;\nimport java.util.Properties;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Class BasicSimianArmyContext.\n */\npublic class BasicSimianArmyContext implements Monkey.Context {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicSimianArmyContext.class);\n\n    /** The configuration properties. */\n    private final Properties properties = new Properties();\n\n    /** The Constant MONKEY_THREADS. */\n    private static final int MONKEY_THREADS = 1;\n\n    /** The scheduler. */\n    private MonkeyScheduler scheduler;\n\n    /** The calendar. */\n    private MonkeyCalendar calendar;\n\n    /** The config. */\n    private BasicConfiguration config;\n\n    /** The client. */\n    private AWSClient client;\n\n    /** The recorder. */\n    private MonkeyRecorder recorder;\n\n    /** The reported events. */\n    private final LinkedList<Event> eventReport;\n\n    /** The AWS credentials provider to be used. */\n    private AWSCredentialsProvider awsCredentialsProvider = new DefaultAWSCredentialsProviderChain();\n\n    /** If configured, the ARN of Role to be assumed. */\n    private final String assumeRoleArn;\n\n    private final String accountName;\n\n    private final String account;\n\n    private final String secret;\n\n    private final String region;\n\n    protected ClientConfiguration awsClientConfig = new ClientConfiguration();\n\n    /* If configured, the proxy to be used when making AWS API requests */\n    private final String proxyHost;\n\n    private final String proxyPort;\n\n    private final String proxyUsername;\n\n    private final String proxyPassword;\n\n    /** The key name of the tag owner used to tag resources - across all Monkeys */\n    public static String GLOBAL_OWNER_TAGKEY;\n\n    /** protected constructor as the Shell is meant to be subclassed. */\n    protected BasicSimianArmyContext(String... configFiles) {\n        eventReport = new LinkedList<Event>();\n        // Load the config files into props following the provided order.\n        for (String configFile : configFiles) {\n            loadConfigurationFileIntoProperties(configFile);\n        }\n        LOGGER.info(\"The following are properties in the context.\");\n        for (Entry<Object, Object> prop : properties.entrySet()) {\n            Object propertyKey = prop.getKey();\n            if (isSafeToLog(propertyKey)) {\n                LOGGER.info(String.format(\"%s = %s\", propertyKey, prop.getValue()));\n            } else {\n                LOGGER.info(String.format(\"%s = (not shown here)\", propertyKey));\n            }\n        }\n\n        config = new BasicConfiguration(properties);\n\n        account = config.getStr(\"simianarmy.client.aws.accountKey\");\n        secret = config.getStr(\"simianarmy.client.aws.secretKey\");\n        accountName = config.getStrOrElse(\"simianarmy.client.aws.accountName\", \"Default\");\n\n        String defaultRegion = \"us-east-1\";\n        Region currentRegion = Regions.getCurrentRegion();\n\n        if (currentRegion != null) {\n            defaultRegion = currentRegion.getName();\n        }\n\n        region = config.getStrOrElse(\"simianarmy.client.aws.region\", defaultRegion);\n        GLOBAL_OWNER_TAGKEY = config.getStrOrElse(\"simianarmy.tags.owner\", \"owner\");\n\n        // Check for and configure optional proxy configuration\n        proxyHost = config.getStr(\"simianarmy.client.aws.proxyHost\");\n        proxyPort = config.getStr(\"simianarmy.client.aws.proxyPort\");\n        proxyUsername = config.getStr(\"simianarmy.client.aws.proxyUser\");\n        proxyPassword = config.getStr(\"simianarmy.client.aws.proxyPassword\");\n        if ((proxyHost != null) && (proxyPort != null)) {\n            awsClientConfig.setProxyHost(proxyHost);\n            awsClientConfig.setProxyPort(Integer.parseInt(proxyPort));\n            if ((proxyUsername != null) && (proxyPassword != null)) {\n                awsClientConfig.setProxyUsername(proxyUsername);\n                awsClientConfig.setProxyPassword(proxyPassword);\n            }\n        }\n\n        assumeRoleArn = config.getStr(\"simianarmy.client.aws.assumeRoleArn\");\n        if (assumeRoleArn != null) {\n            this.awsCredentialsProvider = new STSAssumeRoleSessionCredentialsProvider(assumeRoleArn, awsClientConfig);\n            LOGGER.info(\"Using STSAssumeRoleSessionCredentialsProvider with assume role \" + assumeRoleArn);\n        }\n\n        // if credentials are set explicitly make them available to the AWS SDK\n        if (StringUtils.isNotBlank(account) && StringUtils.isNotBlank(secret)) {\n            this.exportCredentials(account, secret);\n        }\n\n        createClient();\n\n        createCalendar();\n\n        createScheduler();\n\n        createRecorder();\n\n    }\n\n    /**\n     * Checks whether it is safe to log the property based on the given\n     * property key.\n     * @param propertyKey The key for the property, expected to be resolvable to a String\n     * @return A boolean indicating whether it is safe to log the corresponding property\n     */\n    protected boolean isSafeToLog(Object propertyKey) {\n        String propertyKeyName = propertyKey.toString();\n        return !propertyKeyName.contains(\"secretKey\")\n                && !propertyKeyName.contains(\"vsphere.password\");\n    }\n\n    /** loads the given config on top of the config read by previous calls. */\n    protected void loadConfigurationFileIntoProperties(String propertyFileName) {\n        String propFile = System.getProperty(propertyFileName, \"/\" + propertyFileName);\n        try {\n        \tLOGGER.info(\"loading properties file: \" + propFile);\n            InputStream is = BasicSimianArmyContext.class.getResourceAsStream(propFile);\n            try {\n                properties.load(is);\n            } finally {\n                is.close();\n            }\n        } catch (Exception e) {\n            String msg = \"Unable to load properties file \" + propFile + \" set System property \\\"\" + propertyFileName\n                    + \"\\\" to valid file\";\n            LOGGER.error(msg);\n            throw new RuntimeException(msg, e);\n        }\n    }\n\n    private void createScheduler() {\n        int freq = (int) config.getNumOrElse(\"simianarmy.scheduler.frequency\", 1);\n        TimeUnit freqUnit = TimeUnit.valueOf(config.getStrOrElse(\"simianarmy.scheduler.frequencyUnit\", \"HOURS\"));\n        int threads = (int) config.getNumOrElse(\"simianarmy.scheduler.threads\", MONKEY_THREADS);\n        setScheduler(new BasicScheduler(freq, freqUnit, threads));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private void createRecorder() {\n        @SuppressWarnings(\"rawtypes\")\n        Class recorderClass = loadClientClass(\"simianarmy.client.recorder.class\");\n        if (recorderClass != null && recorderClass.equals(RDSRecorder.class)) {\n            String dbDriver = configuration().getStr(\"simianarmy.recorder.db.driver\");\n            String dbUser = configuration().getStr(\"simianarmy.recorder.db.user\");\n            String dbPass = configuration().getStr(\"simianarmy.recorder.db.pass\");\n            String dbUrl = configuration().getStr(\"simianarmy.recorder.db.url\");\n            String dbTable = configuration().getStr(\"simianarmy.recorder.db.table\");\n            \n            RDSRecorder rdsRecorder = new RDSRecorder(dbDriver, dbUser, dbPass, dbUrl, dbTable, client.region());\n            rdsRecorder.init();\n            setRecorder(rdsRecorder);        \t\n        } else if (recorderClass == null || recorderClass.equals(SimpleDBRecorder.class)) {\n            String domain = config.getStrOrElse(\"simianarmy.recorder.sdb.domain\", \"SIMIAN_ARMY\");\n            if (client != null) {\n                SimpleDBRecorder simpleDbRecorder = new SimpleDBRecorder(client, domain);\n                simpleDbRecorder.init();\n                setRecorder(simpleDbRecorder);\n            }\n        } else {\n            setRecorder((MonkeyRecorder) factory(recorderClass));\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private void createCalendar() {\n        @SuppressWarnings(\"rawtypes\")\n        Class calendarClass = loadClientClass(\"simianarmy.calendar.class\");\n        if (calendarClass == null || calendarClass.equals(BasicCalendar.class)) {\n            setCalendar(new BasicCalendar(config));\n        } else {\n            setCalendar((MonkeyCalendar) factory(calendarClass));\n        }\n    }\n\n    /**\n     * Create the specific client with region taken from properties.\n     * Override to provide your own client.\n     */\n    protected void createClient() {\n        createClient(region);\n    }\n\n    /**\n     * Create the specific client within passed region, using the appropriate AWS credentials provider\n     * and client configuration.\n     * @param clientRegion\n     */\n    protected void createClient(String clientRegion) {\n        this.client = new AWSClient(clientRegion, awsCredentialsProvider, awsClientConfig);\n        setCloudClient(this.client);\n    }\n\n    /**\n     * Gets the AWS client.\n     * @return the AWS client\n     */\n    public AWSClient awsClient() {\n        return client;\n    }\n\n    /**\n     * Gets the region.\n     * @return the region\n     */\n    public String region() {\n        return region;\n    }\n\n    /**\n     * Gets the accountName\n     * @return the accountName\n     */\n    public String accountName() {\n        return accountName;\n    }\n\n    @Override\n    public void reportEvent(Event evt) {\n        this.eventReport.add(evt);\n    }\n\n    @Override\n    public void resetEventReport() {\n        eventReport.clear();\n    }\n\n    @Override\n    public String getEventReport() {\n        StringBuilder report = new StringBuilder();\n        for (Event event : this.eventReport) {\n            report.append(String.format(\"%s %s (\", event.eventType(), event.id()));\n            boolean isFirst = true;\n            for (Entry<String, String> field : event.fields().entrySet()) {\n                if (!isFirst) {\n                    report.append(\", \");\n                } else {\n                    isFirst = false;\n                }\n                report.append(String.format(\"%s:%s\", field.getKey(), field.getValue()));\n            }\n            report.append(\")\\n\");\n        }\n        return report.toString();\n    }\n\n    /**\n     * Exports credentials as Java system properties\n     * to be picked up by AWS SDK clients.\n     * @param accountKey\n     * @param secretKey\n     */\n    public void exportCredentials(String accountKey, String secretKey) {\n        System.setProperty(\"aws.accessKeyId\", accountKey);\n        System.setProperty(\"aws.secretKey\", secretKey);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public MonkeyScheduler scheduler() {\n        return scheduler;\n    }\n\n    /**\n     * Sets the scheduler.\n     *\n     * @param scheduler\n     *            the new scheduler\n     */\n    protected void setScheduler(MonkeyScheduler scheduler) {\n        this.scheduler = scheduler;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public MonkeyCalendar calendar() {\n        return calendar;\n    }\n\n    /**\n     * Sets the calendar.\n     *\n     * @param calendar\n     *            the new calendar\n     */\n    protected void setCalendar(MonkeyCalendar calendar) {\n        this.calendar = calendar;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public MonkeyConfiguration configuration() {\n        return config;\n    }\n\n    /**\n     * Sets the configuration.\n     *\n     * @param configuration\n     *            the new configuration\n     */\n    protected void setConfiguration(MonkeyConfiguration configuration) {\n        this.config = (BasicConfiguration) configuration;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public CloudClient cloudClient() {\n        return client;\n    }\n\n    /**\n     * Sets the cloud client.\n     *\n     * @param cloudClient\n     *            the new cloud client\n     */\n    protected void setCloudClient(CloudClient cloudClient) {\n        this.client = (AWSClient) cloudClient;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public MonkeyRecorder recorder() {\n        return recorder;\n    }\n\n    /**\n     * Sets the recorder.\n     *\n     * @param recorder\n     *            the new recorder\n     */\n    protected void setRecorder(MonkeyRecorder recorder) {\n        this.recorder = recorder;\n    }\n\n    /**\n     * Gets the configuration properties.\n     * @return the configuration properties\n     */\n    protected Properties getProperties() {\n        return this.properties;\n    }\n\n    /**\n     * Gets the AWS credentials provider.\n     * @return the AWS credentials provider\n     */\n    public AWSCredentialsProvider getAwsCredentialsProvider() {\n        return awsCredentialsProvider;\n    }\n\n    /**\n     * Gets the AWS client configuration.\n     * @return the AWS client configuration\n     */\n    public ClientConfiguration getAwsClientConfig() {\n        return awsClientConfig;\n    }\n\n    /**\n     * Load a class specified by the config; for drop-in replacements.\n     * (Duplicates a method in MonkeyServer; refactor to util?).\n     *\n     * @param key\n     * @return the loaded class or null if the class is not found\n     */\n    @SuppressWarnings(\"rawtypes\")\n    private Class loadClientClass(String key) {\n        ClassLoader classLoader = getClass().getClassLoader();\n        try {\n            String clientClassName = config.getStrOrElse(key, null);\n            if (clientClassName == null || clientClassName.isEmpty()) {\n                LOGGER.info(\"using standard class for \" + key);\n                return null;\n            }\n        Class newClass = classLoader.loadClass(clientClassName);\n            LOGGER.info(\"using \" + key + \" loaded \" + newClass.getCanonicalName());\n            return newClass;\n        } catch (ClassNotFoundException e) {\n            throw new RuntimeException(\"Could not load \" + key, e);\n        }\n    }\n\n    /**\n     * Generic factory to create monkey collateral types.\n     *\n     * @param <T>\n     *            the generic type to create\n     * @param implClass\n     *            the actual concrete type to instantiate.\n     * @return an object of the requested type\n     */\n    private <T> T factory(Class<T> implClass) {\n        try {\n            // then find corresponding ctor\n            for (Constructor<?> ctor : implClass.getDeclaredConstructors()) {\n                Class<?>[] paramTypes = ctor.getParameterTypes();\n                if (paramTypes.length != 1) {\n                    continue;\n                }\n                if (paramTypes[0].getName().endsWith(\"Configuration\")) {\n                    @SuppressWarnings(\"unchecked\")\n                    T impl = (T) ctor.newInstance(config);\n                    return impl;\n                }\n            }\n            // Last ditch; try no-arg.\n            return implClass.newInstance();\n        } catch (Exception e) {\n            LOGGER.error(\"context config error, cannot make an instance of \" + implClass.getName(), e);\n        }\n        return null;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/LocalDbRecorder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentNavigableMap;\n\nimport org.mapdb.Atomic;\nimport org.mapdb.DB;\nimport org.mapdb.DBMaker;\nimport org.mapdb.Fun;\n\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.MonkeyRecorder;\nimport com.netflix.simianarmy.MonkeyType;\nimport com.netflix.simianarmy.chaos.ChaosMonkey;\n\n/**\n * Replacement for SimpleDB on non-AWS: use an embedded db.\n *\n * @author jgardner\n *\n */\npublic class LocalDbRecorder implements MonkeyRecorder {\n\n    private static DB db = null;\n    private static Atomic.Long nextId = null;\n    private static ConcurrentNavigableMap<Fun.Tuple2<Long, Long>, Event> eventMap = null;\n\n    // Upper bound, so we don't fill the disk with monkey events\n    private static final double MAX_EVENTS = 10000;\n    private double maxEvents = MAX_EVENTS;\n\n    private String dbFilename = \"simianarmy_events\";\n\n    private String dbpassword = null;\n\n    /** Constructor.\n     *\n     */\n    public LocalDbRecorder(MonkeyConfiguration configuration) {\n        if (configuration != null) {\n            dbFilename = configuration.getStrOrElse(\"simianarmy.recorder.localdb.file\", null);\n            maxEvents = configuration.getNumOrElse(\"simianarmy.recorder.localdb.max_events\", MAX_EVENTS);\n            dbpassword = configuration.getStrOrElse(\"simianarmy.recorder.localdb.password\", null);\n        }\n    }\n\n    private synchronized void init() {\n        if (nextId != null) {\n            return;\n        }\n        File dbFile = null;\n        dbFile = (dbFilename == null) ? tempDbFile() : new File(dbFilename);\n        if (dbpassword != null) {\n            db = DBMaker.newFileDB(dbFile)\n                    .closeOnJvmShutdown()\n                    .encryptionEnable(dbpassword)\n                    .make();\n        } else {\n            db = DBMaker.newFileDB(dbFile)\n                    .closeOnJvmShutdown()\n                    .make();\n        }\n        eventMap = db.getTreeMap(\"eventMap\");\n        nextId = db.createAtomicLong(\"next\", 1);\n    }\n\n    private static File tempDbFile() {\n        try {\n            final File tmpFile = File.createTempFile(\"mapdb\", \"db\");\n            tmpFile.deleteOnExit();\n            return tmpFile;\n        } catch (IOException e) {\n            throw new RuntimeException(\"Temporary DB file could not be created\", e);\n        }\n    }\n\n    /* (non-Javadoc)\n     * @see com.netflix.simianarmy.MonkeyRecorder#newEvent(MonkeyType, EventType, String, String)\n     */\n    @Override\n    public Event newEvent(MonkeyType monkeyType, EventType eventType, String region,\n            String id) {\n        init();\n        return new MapDbRecorderEvent(monkeyType, eventType, region, id);\n    }\n\n    /* (non-Javadoc)\n     * @see com.netflix.simianarmy.MonkeyRecorder#recordEvent(com.netflix.simianarmy.MonkeyRecorder.Event)\n     */\n    @Override\n    public void recordEvent(Event evt) {\n        init();\n        Fun.Tuple2<Long, Long> id = Fun.t2(evt.eventTime().getTime(),\n                nextId.incrementAndGet());\n\n        if (eventMap.size() + 1 > maxEvents) {\n            eventMap.remove(eventMap.firstKey());\n        }\n        eventMap.put(id, evt);\n        db.commit();\n    }\n\n    /* (non-Javadoc)\n     * @see com.netflix.simianarmy.MonkeyRecorder#findEvents(java.util.Map, java.util.Date)\n     */\n    @Override\n    public List<Event> findEvents(Map<String, String> query, Date after) {\n        init();\n        List<Event> foundEvents = new ArrayList<Event>();\n        for (Event evt : eventMap.tailMap(toKey(after)).values()) {\n            boolean matched = true;\n            for (Map.Entry<String, String> pair : query.entrySet()) {\n                if (pair.getKey().equals(\"id\") && !evt.id().equals(pair.getValue())) {\n                    matched = false;\n                }\n                if (pair.getKey().equals(\"monkeyType\") && !evt.monkeyType().toString().equals(pair.getValue())) {\n                    matched = false;\n                }\n                if (pair.getKey().equals(\"eventType\") && !evt.eventType().toString().equals(pair.getValue())) {\n                    matched = false;\n                }\n            }\n            if (matched) {\n                foundEvents.add(evt);\n            }\n        }\n        return foundEvents;\n    }\n\n    /* (non-Javadoc)\n     * @see com.netflix.simianarmy.MonkeyRecorder#findEvents(MonkeyType, Map, Date)\n     */\n    @Override\n    public List<Event> findEvents(MonkeyType monkeyType, Map<String, String> query,\n            Date after) {\n        Map<String, String> copy = new LinkedHashMap<String, String>(query);\n        copy.put(\"monkeyType\", monkeyType.name());\n        return findEvents(copy, after);\n    }\n\n    /* (non-Javadoc)\n     * @see com.netflix.simianarmy.MonkeyRecorder#findEvents(MonkeyType, EventType, Map, Date)\n     */\n    @Override\n    public List<Event> findEvents(MonkeyType monkeyType, EventType eventType,\n            Map<String, String> query, Date after) {\n        Map<String, String> copy = new LinkedHashMap<String, String>(query);\n        copy.put(\"monkeyType\", monkeyType.name());\n        copy.put(\"eventType\", eventType.name());\n        return findEvents(copy, after);\n    }\n\n    private Fun.Tuple2<Long, Long> toKey(Date date) {\n        return Fun.t2(date.getTime(), 0L);\n    }\n\n    /** Loggable event for LocalDbRecorder.\n     *\n     */\n    public static class MapDbRecorderEvent implements MonkeyRecorder.Event, Serializable {\n        /** The monkey type. */\n        private MonkeyType monkeyType;\n\n        /** The event type. */\n        private EventType eventType;\n\n        /** The event id. */\n        private String id;\n\n        /** The event region. */\n        private String region;\n\n        /** The fields. */\n        private Map<String, String> fields = new HashMap<String, String>();\n\n        /** The event time. */\n        private Date date;\n\n        private static final long serialVersionUID = 1L;\n\n        /** Constructor.\n         * @param monkeyType\n         * @param eventType\n         * @param region\n         * @param id\n         */\n        public MapDbRecorderEvent(MonkeyType monkeyType, EventType eventType,\n                String region, String id) {\n            this.monkeyType = monkeyType;\n            this.eventType = eventType;\n            this.id = id;\n            this.region = region;\n            this.date = new Date();\n        }\n\n        /** Constructor.\n         * @param monkeyType\n         * @param eventType\n         * @param region\n         * @param id\n         * @param time\n         */\n        public MapDbRecorderEvent(MonkeyType monkeyType, EventType eventType,\n                String region, String id, long time) {\n            this.monkeyType = monkeyType;\n            this.eventType = eventType;\n            this.id = id;\n            this.region = region;\n            this.date = new Date(time);\n        }\n\n        /* (non-Javadoc)\n         * @see com.netflix.simianarmy.MonkeyRecorder.Event#id()\n         */\n        @Override\n        public String id() {\n            return id;\n        }\n\n        /* (non-Javadoc)\n         * @see com.netflix.simianarmy.MonkeyRecorder.Event#eventTime()\n         */\n        @Override\n        public Date eventTime() {\n            return new Date(date.getTime());\n        }\n\n        /* (non-Javadoc)\n         * @see com.netflix.simianarmy.MonkeyRecorder.Event#monkeyType()\n         */\n        @Override\n        public MonkeyType monkeyType() {\n            return monkeyType;\n        }\n\n        /* (non-Javadoc)\n         * @see com.netflix.simianarmy.MonkeyRecorder.Event#eventType()\n         */\n        @Override\n        public EventType eventType() {\n            return eventType;\n        }\n\n        /* (non-Javadoc)\n         * @see com.netflix.simianarmy.MonkeyRecorder.Event#region()\n         */\n        @Override\n        public String region() {\n            return region;\n        }\n\n        /* (non-Javadoc)\n         * @see com.netflix.simianarmy.MonkeyRecorder.Event#fields()\n         */\n        @Override\n        public Map<String, String> fields() {\n            return Collections.unmodifiableMap(fields);\n        }\n\n        /* (non-Javadoc)\n         * @see com.netflix.simianarmy.MonkeyRecorder.Event#field(java.lang.String)\n         */\n        @Override\n        public String field(String name) {\n            return fields.get(name);\n        }\n\n        /* (non-Javadoc)\n         * @see com.netflix.simianarmy.MonkeyRecorder.Event#addField(java.lang.String, java.lang.String)\n         */\n        @Override\n        public Event addField(String name, String value) {\n            fields.put(name, value);\n            return this;\n        }\n\n    }\n\n    /** Appears to be used for testing, if so should be moved to a unit test. (2/16/2014, mgeis)\n     * @param args\n     */\n    public static void main(String[] args) {\n        LocalDbRecorder r = new LocalDbRecorder(null);\n        r.init();\n        List<Event> events2 = r.findEvents(new HashMap<String, String>(), new Date(0));\n        for (Event event : events2) {\n            System.out.println(\"Got:\" + event + \": \" + event.eventTime().getTime());\n        }\n        for (int i = 0; i < 10; i++) {\n            Event event = r.newEvent(ChaosMonkey.Type.CHAOS,\n                    ChaosMonkey.EventTypes.CHAOS_TERMINATION, \"1\", \"1\");\n            r.recordEvent(event);\n            System.out.println(\"Added:\" + event + \": \" + event.eventTime().getTime());\n        }\n        List<Event> events = r.findEvents(new HashMap<String, String>(), new Date(0));\n        for (Event event : events) {\n            System.out.println(\"Got:\" + event + \": \" + event.eventTime().getTime());\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/calendars/BavarianCalendar.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.calendars;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.basic.BasicCalendar;\n\nimport java.util.Arrays;\nimport java.util.Calendar;\nimport java.util.Collection;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\n/**\n * The Class BavarianCalendar.\n */\npublic class BavarianCalendar extends BasicCalendar\n{\n    /**\n     * Instantiates a new basic calendar.\n     *\n     * @param cfg  the monkey configuration\n     */\n    public BavarianCalendar(MonkeyConfiguration cfg)\n    {\n        super(cfg);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    protected void loadHolidays(int year) {\n        holidays.clear();\n\n        // these aren't all strictly holidays, but days when engineers will likely\n        // not be in the office to respond to rampaging monkeys\n\n        // first of all, we need easter sunday doy,\n        // because ome other holidays calculated from it\n        int easter = westernEasterDayOfYear(year);\n\n        // new year\n        holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.JANUARY, 1)));\n\n        // epiphanie\n        holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.JANUARY, 6)));\n\n        // good friday, always friday, don't need to check if it's bridge day\n        holidays.add(easter - 2);\n\n        // easter monday, always monday, don't need to check if it's bridge day\n        holidays.add(easter + 1);\n\n        // labor day\n        holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.MAY, 1)));\n\n        // ascension day\n        holidays.addAll(getHolidayWithBridgeDays(year, easter + 39));\n\n        // whit monday, always monday, don't need to check if it's bridge day\n        holidays.add(easter + 50);\n\n        // corpus christi\n        holidays.add(westernEasterDayOfYear(year) + 60);\n\n        // assumption day\n        holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.AUGUST, 15)));\n\n        // german unity day\n        holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.OCTOBER, 3)));\n\n        // all saints\n        holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.NOVEMBER, 1)));\n\n        // monkey goes on christmas vacations between christmas and new year!\n        holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.DECEMBER, 24)));\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 25));\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 26));\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 27));\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 28));\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 29));\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 30));\n        holidays.add(dayOfYear(year, Calendar.DECEMBER, 31));\n\n        // mark the holiday set with the year, so on Jan 1 it will automatically\n        // recalculate the holidays for next year\n        holidays.add(year);\n    }\n\n    /**\n     * Returns collection of holidays, including Monday or Friday\n     * if given holiday is Thuesday or Thursday.\n     *\n     * The behaviour to take Monday as day off if official holiday is Thuesday\n     * and to take Friday as day off if official holiday is Thursday\n     * is specific to [at least] Germany.\n     * We call it, literally, \"bridge day\".\n     *\n     * @param dayOfYear  holiday day of year\n     */\n    private Collection<Integer> getHolidayWithBridgeDays(int year, int dayOfYear) {\n        Calendar holiday = now();\n        holiday.set(Calendar.YEAR, year);\n        holiday.set(Calendar.DAY_OF_YEAR, dayOfYear);\n        int dow = holiday.get(Calendar.DAY_OF_WEEK);\n        int mon = holiday.get(Calendar.MONTH);\n        int dom = holiday.get(Calendar.DAY_OF_MONTH);\n\n        // We don't want to include Monday if Thuesday is January 1.\n        if (dow == Calendar.TUESDAY && dayOfYear != 1)\n            return Arrays.asList(dayOfYear, dayOfYear - 1);\n\n        // We don't want to include Friday if Thursday is December 31.\n        if (dow == Calendar.THURSDAY && (mon != Calendar.DECEMBER || dom != 31))\n            return Arrays.asList(dayOfYear, dayOfYear + 1);\n\n        return Arrays.asList(dayOfYear);\n    }\n\n    /**\n     * Western easter sunday in year.\n     *\n     * @param year\n     *            the year\n     * @return the day of the year of western easter sunday\n     */\n    protected int westernEasterDayOfYear(int year) {\n        int a = year % 19,\n                b = year / 100,\n                c = year % 100,\n                d = b / 4,\n                e = b % 4,\n                g = (8 * b + 13) / 25,\n                h = (19 * a + b - d - g + 15) % 30,\n                j = c / 4,\n                k = c % 4,\n                m = (a + 11 * h) / 319,\n                r = (2 * e + 2 * j - k - h + m + 32) % 7;\n        int oneBasedMonth = (h - m + r + 90) / 25;\n        int dayOfYear = (h - m + r + oneBasedMonth + 19) % 32;\n        return dayOfYear(year, oneBasedMonth - 1, dayOfYear);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/chaos/BasicChaosEmailNotifier.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.chaos;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport com.netflix.simianarmy.chaos.ChaosEmailNotifier;\nimport com.netflix.simianarmy.chaos.ChaosType;\n\n/** The basic implementation of the email notifier for Chaos monkey.\n *\n */\npublic class BasicChaosEmailNotifier extends ChaosEmailNotifier {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicChaosEmailNotifier.class);\n\n    private final MonkeyConfiguration cfg;\n    private final String defaultEmail;\n    private final List<String> ccAddresses;\n\n    /** Constructor.\n     *\n     * @param cfg the monkey configuration\n     * @param sesClient the Amazon SES client\n     * @param defaultEmail the default email address to notify when the group does not have a\n     * owner email specified\n     * @param ccAddresses the cc email addresses for notifications\n     */\n    public BasicChaosEmailNotifier(MonkeyConfiguration cfg, AmazonSimpleEmailServiceClient sesClient,\n            String defaultEmail, String... ccAddresses) {\n        super(sesClient);\n        this.cfg = cfg;\n        this.defaultEmail = defaultEmail;\n        this.ccAddresses = Arrays.asList(ccAddresses);\n    }\n\n    /**\n     * Sends an email notification for a termination of instance to a global\n     * email address.\n     * @param group the instance group\n     * @param instanceId the instance id\n     * @param chaosType the chosen chaos strategy\n     */\n    @Override\n    public void sendTerminationGlobalNotification(InstanceGroup group, String instanceId, ChaosType chaosType) {\n        String to = cfg.getStr(\"simianarmy.chaos.notification.global.receiverEmail\");\n        if (StringUtils.isBlank(to)) {\n            LOGGER.warn(\"Global email address was not set, but global email notification was enabled!\");\n            return;\n        }\n        LOGGER.info(\"sending termination notification to global email address {}\", to);\n        buildAndSendEmail(to, group, instanceId, chaosType);\n    }\n\n    /**\n     * Sends an email notification for a termination of instance to the group\n     * owner's email address.\n     * @param group the instance group\n     * @param instanceId the instance id\n     * @param chaosType the chosen chaos strategy\n     */\n    @Override\n    public void sendTerminationNotification(InstanceGroup group, String instanceId, ChaosType chaosType) {\n        String to = getOwnerEmail(group);\n        LOGGER.info(\"sending termination notification to group owner email address {}\", to);\n        buildAndSendEmail(to, group, instanceId, chaosType);\n    }\n\n    /**\n     * Gets the owner's email for a instance group.\n     * @param group the instance group\n     * @return the owner email of the instance group\n     */\n    protected String getOwnerEmail(InstanceGroup group) {\n        String prop = String.format(\"simianarmy.chaos.%s.%s.ownerEmail\", group.type(), group.name());\n        String ownerEmail = cfg.getStr(prop);\n        if (ownerEmail == null) {\n            LOGGER.info(String.format(\"Property %s is not set, use the default email address %s as\"\n                    + \" the owner email of group %s of type %s\",\n                    prop, defaultEmail, group.name(), group.type()));\n            return defaultEmail;\n        } else {\n            return ownerEmail;\n        }\n    }\n\n    /**\n     * Builds the body and subject for the email, sends the email.\n     * @param group\n     *          the instance group\n     * @param instanceId\n     *          the instance id\n     * @param to\n     *          the email address to be sent to\n     * @param chaosType the chosen chaos strategy\n     */\n    public void buildAndSendEmail(String to, InstanceGroup group, String instanceId, ChaosType chaosType) {\n        String body = buildEmailBody(group, instanceId, chaosType);\n\n        String subject;\n        boolean emailSubjectIsBody = cfg.getBoolOrElse(\n                \"simianarmy.chaos.notification.subject.isBody\", false);\n        if (emailSubjectIsBody) {\n            subject = body;\n        } else {\n            subject = buildEmailSubject(to);\n        }\n\n        sendEmail(to, subject, body);\n    }\n\n    @Override\n    public String buildEmailSubject(String to) {\n        String emailSubjectPrefix = cfg.getStrOrElse(\"simianarmy.chaos.notification.subject.prefix\", \"\");\n        String emailSubjectSuffix = cfg.getStrOrElse(\"simianarmy.chaos.notification.subject.suffix\", \"\");\n        return String.format(\"%sChaos Monkey Termination Notification for %s%s\",\n                                                emailSubjectPrefix, to, emailSubjectSuffix);\n    }\n\n    /**\n     * Builds the body for the email.\n     * @param group\n     *          the instance group\n     * @param instanceId\n     *          the instance id\n     * @param chaosType the chosen chaos strategy\n     * @return the created string\n     */\n    public String buildEmailBody(InstanceGroup group, String instanceId, ChaosType chaosType) {\n        String emailBodyPrefix = cfg.getStrOrElse(\"simianarmy.chaos.notification.body.prefix\", \"\");\n        String emailBodySuffix = cfg.getStrOrElse(\"simianarmy.chaos.notification.body.suffix\", \"\");\n        String body = emailBodyPrefix;\n        body += String.format(\"Instance %s of %s %s is being terminated by Chaos monkey.\",\n                    instanceId, group.type(), group.name());\n        if (chaosType != null) {\n            body += \"\\n\";\n            body += String.format(\"Chaos type: %s.\", chaosType.getKey());\n        }\n        body += emailBodySuffix;\n        return body;\n    }\n\n    @Override\n    public String[] getCcAddresses(String to) {\n        return ccAddresses.toArray(new String[ccAddresses.size()]);\n    }\n\n    @Override\n    public String getSourceAddress(String to) {\n        String prop = \"simianarmy.chaos.notification.sourceEmail\";\n        String sourceEmail = cfg.getStr(prop);\n        if (sourceEmail == null || !isValidEmail(sourceEmail)) {\n            String msg = String.format(\"Property %s is not set or its value is not a valid email.\", prop);\n            LOGGER.error(msg);\n            throw new RuntimeException(msg);\n        }\n        return sourceEmail;\n    }\n}"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/chaos/BasicChaosInstanceSelector.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.chaos;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Random;\n\nimport com.google.common.collect.Lists;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport com.netflix.simianarmy.chaos.ChaosInstanceSelector;\n\n/**\n * The Class BasicChaosInstanceSelector.\n */\npublic class BasicChaosInstanceSelector implements ChaosInstanceSelector {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicChaosInstanceSelector.class);\n\n    /** The Constant RANDOM. */\n    private static final Random RANDOM = new Random();\n\n    /**\n     * Logger, this is abstracted so subclasses (for testing) can reset logger to make it less verbose.\n     * @return the logger\n     */\n    protected Logger logger() {\n        return LOGGER;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Collection<String> select(InstanceGroup group, double probability) {\n        int n = ((int) probability);\n        String selected = selectOneInstance(group, probability - n);\n        Collection<String> result = selectNInstances(group.instances(), n, selected);\n        if (selected != null) {\n            result.add(selected);\n        }\n        return result;\n    }\n\n    private Collection<String> selectNInstances(Collection<String> instances, int n, String selected) {\n        logger().info(\"Randomly selecting {} from {} instances, excluding {}\",\n                new Object[] {n, instances.size(), selected});\n        List<String> copy = Lists.newArrayList();\n        for (String instance : instances) {\n            if (!instance.equals(selected)) {\n                copy.add(instance);\n            }\n        }\n        if (n >= copy.size()) {\n            return copy;\n        }\n        Collections.shuffle(copy);\n        return copy.subList(0, n);\n    }\n\n    private String selectOneInstance(InstanceGroup group, double probability) {\n        Validate.isTrue(probability < 1);\n        if (probability <= 0) {\n            logger().info(\"Group {} [type {}] has disabled probability: {}\",\n                    new Object[] {group.name(), group.type(), probability});\n            return null;\n        }\n        double rand = Math.random();\n        if (rand > probability || group.instances().isEmpty()) {\n            logger().info(\"Group {} [type {}] got lucky: {} > {}\",\n                    new Object[] {group.name(), group.type(), rand, probability});\n            return null;\n        }\n        return group.instances().get(RANDOM.nextInt(group.instances().size()));\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/chaos/BasicChaosMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.chaos;\n\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.*;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.chaos.*;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * The Class BasicChaosMonkey.\n */\npublic class BasicChaosMonkey extends ChaosMonkey {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicChaosMonkey.class);\n\n    /** The Constant NS. */\n    private static final String NS = \"simianarmy.chaos.\";\n\n    /** The cfg. */\n    private final MonkeyConfiguration cfg;\n\n    /** The runs per day. */\n    private final long runsPerDay;\n\n    /** The minimum value of the maxTerminationCountPerday property to be considered non-zero. **/\n    private static final double MIN_MAX_TERMINATION_COUNT_PER_DAY = 0.001;\n\n    private final MonkeyCalendar monkeyCalendar;\n\n    // When a mandatory termination is triggered due to the minimum termination limit is breached,\n    // the value below is used as the termination probability.\n    private static final double DEFAULT_MANDATORY_TERMINATION_PROBABILITY = 0.5;\n\n    private final List<ChaosType> allChaosTypes;\n\n    /**\n     * Instantiates a new basic chaos monkey.\n     * @param ctx\n     *            the ctx\n     */\n    public BasicChaosMonkey(ChaosMonkey.Context ctx) {\n        super(ctx);\n\n        this.cfg = ctx.configuration();\n        this.monkeyCalendar = ctx.calendar();\n\n        Calendar open = monkeyCalendar.now();\n        Calendar close = monkeyCalendar.now();\n        open.set(Calendar.HOUR, monkeyCalendar.openHour());\n        close.set(Calendar.HOUR, monkeyCalendar.closeHour());\n\n        allChaosTypes = Lists.newArrayList();\n        allChaosTypes.add(new ShutdownInstanceChaosType(cfg));\n        allChaosTypes.add(new BlockAllNetworkTrafficChaosType(cfg));\n        allChaosTypes.add(new DetachVolumesChaosType(cfg));\n        allChaosTypes.add(new BurnCpuChaosType(cfg));\n        allChaosTypes.add(new BurnIoChaosType(cfg));\n        allChaosTypes.add(new KillProcessesChaosType(cfg));\n        allChaosTypes.add(new NullRouteChaosType(cfg));\n        allChaosTypes.add(new FailEc2ChaosType(cfg));\n        allChaosTypes.add(new FailDnsChaosType(cfg));\n        allChaosTypes.add(new FailDynamoDbChaosType(cfg));\n        allChaosTypes.add(new FailS3ChaosType(cfg));\n        allChaosTypes.add(new FillDiskChaosType(cfg));\n        allChaosTypes.add(new NetworkCorruptionChaosType(cfg));\n        allChaosTypes.add(new NetworkLatencyChaosType(cfg));\n        allChaosTypes.add(new NetworkLossChaosType(cfg));\n\n        TimeUnit freqUnit = ctx.scheduler().frequencyUnit();\n        if (TimeUnit.DAYS == freqUnit) {\n            runsPerDay = ctx.scheduler().frequency();\n        } else {\n            long units = freqUnit.convert(close.getTimeInMillis() - open.getTimeInMillis(), TimeUnit.MILLISECONDS);\n            runsPerDay = units / ctx.scheduler().frequency();\n        }\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void doMonkeyBusiness() {\n            context().resetEventReport();\n            cfg.reload();\n            if (!isChaosMonkeyEnabled()) {\n                return;\n            }\n            for (InstanceGroup group : context().chaosCrawler().groups()) {\n                if (isGroupEnabled(group)) {\n                    if (isMaxTerminationCountExceeded(group)) {\n                        continue;\n                    }\n                    double prob = getEffectiveProbability(group);\n                    Collection<String> instances = context().chaosInstanceSelector().select(group, prob / runsPerDay);\n                    for (String inst : instances) {\n                        if (isMaxTerminationCountExceeded(group)) {\n                            break;\n                        }\n                        ChaosType chaosType = pickChaosType(context().cloudClient(), inst);\n                        if (chaosType == null) {\n                            // This is surprising ... normally we can always just terminate it\n                            LOGGER.warn(\"No chaos type was applicable to the instance: {}\", inst);\n                            continue;\n                        }\n                        terminateInstance(group, inst, chaosType);\n                    }\n                }\n            }\n    }\n\n    private ChaosType pickChaosType(CloudClient cloudClient, String instanceId) {\n        Random random = new Random();\n\n        SshConfig sshConfig = new SshConfig(cfg);\n        ChaosInstance instance = new ChaosInstance(cloudClient, instanceId, sshConfig);\n\n        List<ChaosType> applicable = Lists.newArrayList();\n        for (ChaosType chaosType : allChaosTypes) {\n            if (chaosType.isEnabled() && chaosType.canApply(instance)) {\n                applicable.add(chaosType);\n            }\n        }\n\n        if (applicable.isEmpty()) {\n            return null;\n        }\n\n        int index = random.nextInt(applicable.size());\n        return applicable.get(index);\n    }\n\n    @Override\n    public Event terminateNow(String type, String name, ChaosType chaosType)\n            throws FeatureNotEnabledException, InstanceGroupNotFoundException {\n        Validate.notNull(type);\n        Validate.notNull(name);\n        cfg.reload(name);\n        if (!isChaosMonkeyEnabled()) {\n            String msg = String.format(\"Chaos monkey is not enabled for group %s [type %s]\",\n                    name, type);\n            LOGGER.info(msg);\n            throw new FeatureNotEnabledException(msg);\n        }\n        String prop = NS + \"terminateOndemand.enabled\";\n        if (cfg.getBool(prop)) {\n            InstanceGroup group = findInstanceGroup(type, name);\n            if (group == null) {\n                throw new InstanceGroupNotFoundException(type, name);\n            }\n            Collection<String> instances = context().chaosInstanceSelector().select(group, 1.0);\n            Validate.isTrue(instances.size() <= 1);\n            if (instances.size() == 1) {\n                return terminateInstance(group, instances.iterator().next(), chaosType);\n            } else {\n                throw new NotFoundException(String.format(\"No instance is found in group %s [type %s]\",\n                        name, type));\n            }\n        } else {\n            String msg = String.format(\"Group %s [type %s] does not allow on-demand termination, set %s=true\",\n                    name, type, prop);\n            LOGGER.info(msg);\n            throw new FeatureNotEnabledException(msg);\n        }\n    }\n\n    private void reportEventForSummary(EventTypes eventType, InstanceGroup group, String instanceId) {\n        context().reportEvent(createEvent(eventType, group, instanceId));\n    }\n\n    /**\n     * Handle termination error. This has been abstracted so subclasses can decide to continue causing chaos if desired.\n     *\n     * @param instance\n     *            the instance\n     * @param e\n     *            the exception\n     */\n    protected void handleTerminationError(String instance, Throwable e) {\n        LOGGER.error(\"failed to terminate instance \" + instance, e);\n        throw new RuntimeException(\"failed to terminate instance \" + instance, e);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Event recordTermination(InstanceGroup group, String instance, ChaosType chaosType) {\n        Event evt = context().recorder().newEvent(Type.CHAOS, EventTypes.CHAOS_TERMINATION, group.region(), instance);\n        evt.addField(\"groupType\", group.type().name());\n        evt.addField(\"groupName\", group.name());\n        evt.addField(\"chaosType\", chaosType.getKey());\n        context().recorder().recordEvent(evt);\n        return evt;\n    }\n    \n    /** {@inheritDoc} */\n    @Override\n    public int getPreviousTerminationCount(InstanceGroup group, Date after) {\n        Map<String, String> query = new HashMap<String, String>();\n        query.put(\"groupType\", group.type().name());\n        query.put(\"groupName\", group.name());\n        List<Event> evts = context().recorder().findEvents(Type.CHAOS, EventTypes.CHAOS_TERMINATION, query, after);\n        return evts.size();\n    }\n\n    private Event createEvent(EventTypes chaosTermination, InstanceGroup group, String instance) {\n        Event evt = context().recorder().newEvent(Type.CHAOS, chaosTermination, group.region(), instance);\n        evt.addField(\"groupType\", group.type().name());\n        evt.addField(\"groupName\", group.name());\n        return evt;\n    }\n\n    /**\n     * Gets the effective probability value, returns 0 if the group is not enabled. Otherwise calls\n     * getEffectiveProbability.\n     * @param group\n     * @return the effective probability value for the instance group\n     */\n    protected double getEffectiveProbability(InstanceGroup group) {\n        if (!isGroupEnabled(group)) {\n            return 0;\n        }\n        return getEffectiveProbabilityFromCfg(group);\n    }\n\n    /**\n     * Gets the effective probability value when the monkey processes an instance group, it uses the following\n     * logic in the order as listed below.\n     *\n     * 1) When minimum mandatory termination is enabled, a default non-zero probability is used for opted-in\n     * groups, if a) the application has been opted in for the last mandatory termination window\n     *        and b) there was no terminations in the last mandatory termination window\n     * 2) Use the probability configured for the group type and name\n     * 3) Use the probability configured for the group\n     * 4) Use 1.0\n     * @param group\n     * @return double\n     */\n    protected double getEffectiveProbabilityFromCfg(InstanceGroup group) {\n        String propName;\n        if (cfg.getBool(NS + \"mandatoryTermination.enabled\")) {\n            String mtwProp = NS + \"mandatoryTermination.windowInDays\";\n            int mandatoryTerminationWindowInDays = (int) cfg.getNumOrElse(mtwProp, 0);\n            if (mandatoryTerminationWindowInDays > 0\n                    && noTerminationInLastWindow(group, mandatoryTerminationWindowInDays)) {\n                double mandatoryProb = cfg.getNumOrElse(NS + \"mandatoryTermination.defaultProbability\",\n                        DEFAULT_MANDATORY_TERMINATION_PROBABILITY);\n                LOGGER.info(\"There has been no terminations for group {} [type {}] in the last {} days,\"\n                        + \"setting the probability to {} for mandatory termination.\",\n                        new Object[]{group.name(), group.type(), mandatoryTerminationWindowInDays, mandatoryProb});\n                return mandatoryProb;\n            }\n        }\n        propName = \"probability\";\n        double prob = getNumFromCfgOrDefault(group, propName, 1.0);\n        LOGGER.info(\"Group {} [type {}] enabled [prob {}]\", new Object[]{group.name(), group.type(), prob});\n        return prob;\n    }\n\n    protected double getNumFromCfgOrDefault(InstanceGroup group, String propName, double defaultValue) {\n        String defaultProp = String.format(\"%s%s.%s\", NS, group.type(), propName);\n        String prop = String.format(\"%s%s.%s.%s\", NS, group.type(), group.name(), propName);\n        return cfg.getNumOrElse(prop, cfg.getNumOrElse(defaultProp, defaultValue));\n    }\n\n    protected boolean getBoolFromCfgOrDefault(InstanceGroup group, String propName, boolean defaultValue) {\n        String defaultProp = String.format(\"%s%s.%s\", NS, group.type(), propName);\n        String prop = String.format(\"%s%s.%s.%s\", NS, group.type(), group.name(), propName);\n        return cfg.getBoolOrElse(prop, cfg.getBoolOrElse(defaultProp, defaultValue));\n    }\n\n    /**\n     * Returns lastOptInTimeInMilliseconds from the .properties file.\n     *\n     * @param group\n     * @return long\n     */\n    protected long getLastOptInMilliseconds(InstanceGroup group) {\n        String prop = NS + group.type() + \".\" + group.name() + \".lastOptInTimeInMilliseconds\";\n        long lastOptInTimeInMilliseconds = (long) cfg.getNumOrElse(prop, -1);\n        return lastOptInTimeInMilliseconds;\n    }\n\n    private boolean noTerminationInLastWindow(InstanceGroup group, int mandatoryTerminationWindowInDays) {\n    long lastOptInTimeInMilliseconds = getLastOptInMilliseconds(group);\n        if (lastOptInTimeInMilliseconds < 0) {\n            return false;\n        }\n\n        Calendar windowStart = monkeyCalendar.now();\n        windowStart.add(Calendar.DATE, -1 * mandatoryTerminationWindowInDays);\n\n        // return true if the window start is after the last opt-in time and\n        // there has been no termination since the window start\n        if (windowStart.getTimeInMillis() > lastOptInTimeInMilliseconds\n                && getPreviousTerminationCount(group, windowStart.getTime()) <= 0) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Checks to see if the given instance group is enabled.\n     * @param group\n     * @return boolean\n     */\n    protected boolean isGroupEnabled(InstanceGroup group) {\n        boolean enabled = getBoolFromCfgOrDefault(group, \"enabled\", false);\n        if (enabled) {\n            return true;\n        } else {\n            String prop = NS + group.type() + \".\" + group.name() + \".enabled\";\n            String defaultProp = NS + group.type() + \".enabled\";\n            LOGGER.info(\"Group {} [type {}] disabled, set {}=true or {}=true\",\n                    new Object[]{group.name(), group.type(), prop, defaultProp});\n            return false;\n        }\n    }\n\n    private boolean isChaosMonkeyEnabled() {\n        String prop = NS + \"enabled\";\n        if (cfg.getBoolOrElse(prop, true)) {\n            return true;\n        }\n        LOGGER.info(\"ChaosMonkey disabled, set {}=true\", prop);\n        return false;\n    }\n\n    private InstanceGroup findInstanceGroup(String type, String name) {\n        // Calling context().chaosCrawler().groups(name) causes a new crawl to get\n        // the up to date information for the group name.\n        for (InstanceGroup group : context().chaosCrawler().groups(name)) {\n            if (group.type().toString().equals(type) && group.name().equals(name)) {\n                return group;\n            }\n        }\n        LOGGER.warn(\"Failed to find instance group for type {} and name {}\", type, name);\n        return null;\n    }\n\n    protected Event terminateInstance(InstanceGroup group, String inst, ChaosType chaosType) {\n        Validate.notNull(group);\n        Validate.notEmpty(inst);\n        String prop = NS + \"leashed\";\n        if (cfg.getBoolOrElse(prop, true)) {\n            LOGGER.info(\"leashed ChaosMonkey prevented from killing {} from group {} [{}], set {}=false\",\n                    new Object[]{inst, group.name(), group.type(), prop});\n            reportEventForSummary(EventTypes.CHAOS_TERMINATION_SKIPPED, group, inst);\n            return null;\n        } else {\n            try {\n                Event evt = recordTermination(group, inst, chaosType);\n                sendTerminationNotification(group, inst, chaosType);\n                SshConfig sshConfig = new SshConfig(cfg);\n                ChaosInstance chaosInstance = new ChaosInstance(context().cloudClient(), inst, sshConfig);\n                chaosType.apply(chaosInstance);\n                LOGGER.info(\"Terminated {} from group {} [{}] with {}\",\n                        new Object[]{inst, group.name(), group.type(), chaosType.getKey() });\n                reportEventForSummary(EventTypes.CHAOS_TERMINATION, group, inst);\n                return evt;\n            } catch (NotFoundException e) {\n                LOGGER.warn(\"Failed to terminate \" + inst + \", it does not exist. Perhaps it was already terminated\");\n                reportEventForSummary(EventTypes.CHAOS_TERMINATION_SKIPPED, group, inst);\n                return null;\n            } catch (Exception e) {\n                handleTerminationError(inst, e);\n                reportEventForSummary(EventTypes.CHAOS_TERMINATION_SKIPPED, group, inst);\n                return null;\n            }\n        }\n    }\n\n    /**\n     * Checks to see if the maximum termination window has been exceeded.\n     *\n     * @param group\n     * @return boolean\n     */\n    protected boolean isMaxTerminationCountExceeded(InstanceGroup group) {\n        Validate.notNull(group);\n        String propName = \"maxTerminationsPerDay\";\n        double maxTerminationsPerDay = getNumFromCfgOrDefault(group, propName, 1.0);\n        if (maxTerminationsPerDay <= MIN_MAX_TERMINATION_COUNT_PER_DAY) {\n            String prop = String.format(\"%s%s.%s.%s\", NS, group.type(), group.name(), propName);\n            LOGGER.info(\"ChaosMonkey is configured to not allow any killing from group {} [{}] \"\n                    + \"with max daily count set as {}\", new Object[]{group.name(), group.type(), prop});\n            return true;\n        } else {\n            int daysBack = 1;\n            int maxCount = (int) maxTerminationsPerDay;\n            if (maxTerminationsPerDay < 1.0) {\n                daysBack = (int) Math.ceil(1 / maxTerminationsPerDay);\n                maxCount = 1;\n            }\n            Calendar after = monkeyCalendar.now();\n            after.add(Calendar.DATE, -1 * daysBack);\n            // Check if the group has exceeded the maximum terminations for the last period\n            int terminationCount = getPreviousTerminationCount(group, after.getTime());\n            if (terminationCount >= maxCount) {\n                LOGGER.info(\"The count of terminations for group {} [{}] in the last {} days is {},\"\n                        + \" equal or greater than the max count threshold {}\",\n                        new Object[]{group.name(), group.type(), daysBack, terminationCount, maxCount});\n                return true;\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public void sendTerminationNotification(InstanceGroup group, String instance, ChaosType chaosType) {\n        String propEmailGlobalEnabled = \"simianarmy.chaos.notification.global.enabled\";\n        String propEmailGroupEnabled = String.format(\"%s%s.%s.notification.enabled\", NS, group.type(), group.name());\n\n        ChaosEmailNotifier notifier = context().chaosEmailNotifier();\n        if (notifier == null) {\n            String msg = \"Chaos email notifier is not set.\";\n            LOGGER.error(msg);\n            throw new RuntimeException(msg);\n        }\n        if (cfg.getBoolOrElse(propEmailGroupEnabled, false)) {\n            notifier.sendTerminationNotification(group, instance, chaosType);\n        }\n        if (cfg.getBoolOrElse(propEmailGlobalEnabled, false)) {\n            notifier.sendTerminationGlobalNotification(group, instance, chaosType);\n        }\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<ChaosType> getChaosTypes() {\n        return Lists.newArrayList(allChaosTypes);\n    }\n}"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/chaos/BasicInstanceGroup.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.chaos;\n\nimport java.util.Collections;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.netflix.simianarmy.GroupType;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\n\n/**\n * The Class BasicInstanceGroup.\n */\npublic class BasicInstanceGroup implements InstanceGroup {\n\n    /** The name. */\n    private final String name;\n\n    /** The type. */\n    private final GroupType type;\n\n    /** The region. */\n    private final String region;\n\n    /** list of the tags of the ASG */\n    private final List<TagDescription> tags;\n\n    /**\n     * Instantiates a new basic instance group.\n     *\n     * @param name\n     *            the name\n     * @param type\n     *            the type\n     * @param tags\n     *            the ASG tags\n     */\n    public BasicInstanceGroup(String name, GroupType type, String region, List<TagDescription> tags) {\n        this.name = name;\n        this.type = type;\n        this.region = region;\n        this.tags = tags;\n    }\n\n\n\n    /** {@inheritDoc} */\n    public GroupType type() {\n        return type;\n    }\n\n    /** {@inheritDoc} */\n    public String name() {\n        return name;\n    }\n\n    /** {@inheritDoc} */\n    public String region() {\n        return region;\n    }\n\n    /** {@inheritDoc} */\n    public List<TagDescription> tags() {\n        return tags;\n    }\n\n    /** The list. */\n    private List<String> list = new LinkedList<String>();\n\n    /** {@inheritDoc} */\n    @Override\n    public List<String> instances() {\n        return Collections.unmodifiableList(list);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void addInstance(String instance) {\n        list.add(instance);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public BasicInstanceGroup copyAs(String newName) {\n        BasicInstanceGroup newGroup = new BasicInstanceGroup(newName, this.type(), this.region(), this.tags());\n        for (String instance: this.instances()) {\n            newGroup.addInstance(instance);\n        }\n        return newGroup;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/chaos/CloudFormationChaosMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.chaos;\n\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport com.netflix.simianarmy.chaos.ChaosType;\n\n/**\n * The Class CloudFormationChaosMonkey. Strips out the random string generated by the CloudFormation api in\n * the instance group name of the ASG we want to kill instances on\n */\npublic class CloudFormationChaosMonkey extends BasicChaosMonkey {\n\n    /**\n     * Instantiates a new cloud formation chaos monkey.\n     * @param ctx\n     *            the ctx\n     */\n    public CloudFormationChaosMonkey(Context ctx) {\n        super(ctx);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    protected boolean isGroupEnabled(InstanceGroup group) {\n        InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group);\n        return super.isGroupEnabled(noSuffixGroup);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    protected Event terminateInstance(InstanceGroup group, String inst, ChaosType chaosType) {\n        InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group);\n        return super.terminateInstance(noSuffixGroup, inst, chaosType);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    protected boolean isMaxTerminationCountExceeded(InstanceGroup group) {\n        InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group);\n        return super.isMaxTerminationCountExceeded(noSuffixGroup);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    protected double getEffectiveProbability(InstanceGroup group) {\n        InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group);\n        if (!super.isGroupEnabled(noSuffixGroup)) {\n            return 0;\n        }\n        return getEffectiveProbabilityFromCfg(noSuffixGroup);\n    }\n\n    /**\n     * Returns the lastOptInTimeInMilliseconds parameter for a group omitting the\n     * randomly generated suffix.\n     */\n    @Override\n    protected long getLastOptInMilliseconds(InstanceGroup group) {\n        InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group);\n        return super.getLastOptInMilliseconds(noSuffixGroup);\n    }\n\n    /**\n     * Return a copy of the instance group removing the randomly generated suffix from\n     * its name.\n     */\n    public InstanceGroup noSuffixInstanceGroup(InstanceGroup group) {\n        String newName = group.name().replaceAll(\"(-)([^-]*$)\", \"\");\n        InstanceGroup noSuffixGroup = group.copyAs(newName);\n        return noSuffixGroup;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityEmailBuilder.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.conformity;\n\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport com.netflix.simianarmy.conformity.ConformityEmailBuilder;\nimport com.netflix.simianarmy.conformity.ConformityRule;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/** The basic implementation of the email builder for Conformity monkey. */\npublic class BasicConformityEmailBuilder extends ConformityEmailBuilder {\n    private static final String[] TABLE_COLUMNS = {\"Cluster\", \"Region\", \"Rule\", \"Failed Components\"};\n    private static final String AHREF_TEMPLATE = \"<a href=\\\"%s\\\">%s</a>\";\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicConformityEmailBuilder.class);\n\n    private Map<String, Collection<Cluster>> emailToClusters;\n    private final Map<String, ConformityRule> idToRule = Maps.newHashMap();\n\n    @Override\n    public void setEmailToClusters(Map<String, Collection<Cluster>> clustersByEmail, Collection<ConformityRule> rules) {\n        Validate.notNull(clustersByEmail);\n        Validate.notNull(rules);\n        this.emailToClusters = clustersByEmail;\n        idToRule.clear();\n        for (ConformityRule rule : rules) {\n            idToRule.put(rule.getName(), rule);\n        }\n    }\n\n    @Override\n    protected String getHeader() {\n        StringBuilder header = new StringBuilder();\n        header.append(\"<b><h2>Conformity Report</h2></b>\");\n        header.append(\"The following is a list of failed conformity rules for your cluster(s).<br/>\");\n        return header.toString();\n    }\n\n    @Override\n    protected String getEntryTable(String emailAddress) {\n        StringBuilder table = new StringBuilder();\n        table.append(getHtmlTableHeader(getTableColumns()));\n        for (Cluster cluster : emailToClusters.get(emailAddress)) {\n            for (Conformity conformity : cluster.getConformties()) {\n                if (!conformity.getFailedComponents().isEmpty()) {\n                    table.append(getClusterRow(cluster, conformity));\n                }\n            }\n        }\n        table.append(\"</table>\");\n        return table.toString();\n    }\n\n    @Override\n    protected String getFooter() {\n        return \"<br/>Conformity Monkey wiki: https://github.com/Netflix/SimianArmy/wiki<br/>\";\n    }\n\n    /**\n     * Gets the url to view the details of the cluster.\n     * @param cluster the cluster\n     * @return the url to view/edit the cluster.\n     */\n    protected String getClusterUrl(Cluster cluster) {\n        return null;\n    }\n\n    /**\n     * Gets the string when displaying the cluster, e.g. the id.\n     * @param cluster the cluster to display\n     * @return the string to represent the cluster\n     */\n    protected String getClusterDisplay(Cluster cluster) {\n        return cluster.getName();\n    }\n\n    /** Gets the table columns for the table in the email.\n     *\n     * @return the array of column names\n     */\n    protected String[] getTableColumns() {\n        return TABLE_COLUMNS;\n    }\n\n    /**\n     * Gets the row for a cluster and a failed conformity check in the table in the email body.\n     * @param cluster the cluster to display\n     * @param conformity the failed conformity check\n     * @return the table row in the email body\n     */\n    protected String getClusterRow(Cluster cluster, Conformity conformity) {\n        StringBuilder message = new StringBuilder();\n        message.append(\"<tr>\");\n        String clusterUrl = getClusterUrl(cluster);\n        if (!StringUtils.isEmpty(clusterUrl)) {\n            message.append(getHtmlCell(String.format(AHREF_TEMPLATE, clusterUrl, getClusterDisplay(cluster))));\n        } else {\n            message.append(getHtmlCell(getClusterDisplay(cluster)));\n        }\n        message.append(getHtmlCell(cluster.getRegion()));\n        ConformityRule rule = idToRule.get(conformity.getRuleId());\n        String ruleDesc;\n        if (rule == null) {\n            LOGGER.warn(String.format(\"Not found rule with name %s\", conformity.getRuleId()));\n            ruleDesc = conformity.getRuleId();\n        } else {\n            ruleDesc = rule.getNonconformingReason();\n        }\n        message.append(getHtmlCell(ruleDesc));\n        message.append(getHtmlCell(StringUtils.join(conformity.getFailedComponents(), \",\")));\n        message.append(\"</tr>\");\n        return message.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkey.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.conformity;\n\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.ClusterCrawler;\nimport com.netflix.simianarmy.conformity.ConformityClusterTracker;\nimport com.netflix.simianarmy.conformity.ConformityEmailNotifier;\nimport com.netflix.simianarmy.conformity.ConformityMonkey;\nimport com.netflix.simianarmy.conformity.ConformityRuleEngine;\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.Date;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/** The basic implementation of Conformity Monkey. */\npublic class BasicConformityMonkey extends ConformityMonkey {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicConformityMonkey.class);\n\n    /** The Constant NS. */\n    private static final String NS = \"simianarmy.conformity.\";\n\n    /** The cfg. */\n    private final MonkeyConfiguration cfg;\n\n    private final ClusterCrawler crawler;\n\n    private final ConformityEmailNotifier emailNotifier;\n\n    private final Collection<String> regions = Lists.newArrayList();\n\n    private final ConformityClusterTracker clusterTracker;\n\n    private final MonkeyCalendar calendar;\n\n    private final ConformityRuleEngine ruleEngine;\n\n    /** Flag to indicate whether the monkey is leashed. */\n    private boolean leashed;\n\n    /**\n     * Clusters that are not conforming in the last check.\n     */\n    private final Map<String, Collection<Cluster>> nonconformingClusters = Maps.newHashMap();\n\n    /**\n     * Clusters that are conforming in the last check.\n     */\n    private final Map<String, Collection<Cluster>> conformingClusters = Maps.newHashMap();\n\n    /**\n     * Clusters that the monkey failed to check for some reason.\n     */\n    private final Map<String, Collection<Cluster>> failedClusters = Maps.newHashMap();\n\n    /**\n     * Clusters that do not exist in the cloud anymore.\n     */\n    private final Map<String, Collection<Cluster>> nonexistentClusters = Maps.newHashMap();\n\n    /**\n     * Instantiates a new basic conformity monkey.\n     *\n     * @param ctx\n     *            the ctx\n     */\n    public BasicConformityMonkey(Context ctx) {\n        super(ctx);\n        cfg = ctx.configuration();\n        crawler = ctx.clusterCrawler();\n        ruleEngine = ctx.ruleEngine();\n        emailNotifier = ctx.emailNotifier();\n        for (String region : ctx.regions()) {\n            regions.add(region);\n        }\n        clusterTracker = ctx.clusterTracker();\n        calendar = ctx.calendar();\n        leashed = ctx.isLeashed();\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void doMonkeyBusiness() {\n        cfg.reload();\n        context().resetEventReport();\n\n        if (isConformityMonkeyEnabled()) {\n            nonconformingClusters.clear();\n            conformingClusters.clear();\n            failedClusters.clear();\n            nonexistentClusters.clear();\n\n            List<Cluster> clusters = crawler.clusters();\n            Map<String, Set<String>> existingClusterNamesByRegion = Maps.newHashMap();\n            for (String region : regions) {\n                existingClusterNamesByRegion.put(region, new HashSet<String>());\n            }\n            for (Cluster cluster : clusters) {\n                existingClusterNamesByRegion.get(cluster.getRegion()).add(cluster.getName());\n            }\n            List<Cluster> trackedClusters = clusterTracker.getAllClusters(regions.toArray(new String[regions.size()]));\n            for (Cluster trackedCluster : trackedClusters) {\n                if (!existingClusterNamesByRegion.get(trackedCluster.getRegion()).contains(trackedCluster.getName())) {\n                    addCluster(nonexistentClusters, trackedCluster);\n                }\n            }\n            for (String region : regions) {\n                Collection<Cluster> toDelete = nonexistentClusters.get(region);\n                if (toDelete != null) {\n                    clusterTracker.deleteClusters(toDelete.toArray(new Cluster[toDelete.size()]));\n                }\n            }\n\n            LOGGER.info(String.format(\"Performing conformity check for %d crawled clusters.\", clusters.size()));\n            Date now = calendar.now().getTime();\n            for (Cluster cluster : clusters) {\n                boolean conforming;\n                try {\n                    conforming = ruleEngine.check(cluster);\n                } catch (Exception e) {\n                    LOGGER.error(String.format(\"Failed to perform conformity check for cluster %s\", cluster.getName()),\n                            e);\n                    addCluster(failedClusters, cluster);\n                    continue;\n                }\n                cluster.setUpdateTime(now);\n                cluster.setConforming(conforming);\n                if (conforming) {\n                    LOGGER.info(String.format(\"Cluster %s is conforming\", cluster.getName()));\n                    addCluster(conformingClusters, cluster);\n                } else {\n                    LOGGER.info(String.format(\"Cluster %s is not conforming\", cluster.getName()));\n                    addCluster(nonconformingClusters, cluster);\n                }\n                if (!leashed) {\n                    LOGGER.info(String.format(\"Saving cluster %s\", cluster.getName()));\n                    clusterTracker.addOrUpdate(cluster);\n                } else {\n                    LOGGER.info(String.format(\n                            \"The conformity monkey is leashed, no data change is made for cluster %s.\",\n                            cluster.getName()));\n                }\n            }\n            if (!leashed) {\n                emailNotifier.sendNotifications();\n            } else {\n                LOGGER.info(\"Conformity monkey is leashed, no notification is sent.\");\n            }\n            if (cfg.getBoolOrElse(NS + \"summaryEmail.enabled\", true)) {\n                sendConformitySummaryEmail();\n            }\n        }\n    }\n\n    private static void addCluster(Map<String, Collection<Cluster>> map, Cluster cluster) {\n        Collection<Cluster> clusters = map.get(cluster.getRegion());\n        if (clusters == null) {\n            clusters = Lists.newArrayList();\n            map.put(cluster.getRegion(), clusters);\n        }\n        clusters.add(cluster);\n    }\n\n    /**\n     * Send a summary email with about the last run of the conformity monkey.\n     */\n    protected void sendConformitySummaryEmail() {\n        String summaryEmailTarget = cfg.getStr(NS + \"summaryEmail.to\");\n        if (!StringUtils.isEmpty(summaryEmailTarget)) {\n            if (!emailNotifier.isValidEmail(summaryEmailTarget)) {\n                LOGGER.error(String.format(\"The email target address '%s' for Conformity summary email is invalid\",\n                        summaryEmailTarget));\n                return;\n            }\n            StringBuilder message = new StringBuilder();\n            for (String region : regions) {\n                appendSummary(message, \"nonconforming\", nonconformingClusters, region, true);\n                appendSummary(message, \"failed to check\", failedClusters, region, true);\n                appendSummary(message, \"nonexistent\", nonexistentClusters, region, true);\n                appendSummary(message, \"conforming\", conformingClusters, region, false);\n            }\n            String subject = getSummaryEmailSubject();\n            emailNotifier.sendEmail(summaryEmailTarget, subject, message.toString());\n        }\n    }\n\n    private void appendSummary(StringBuilder message, String summaryName,\n                               Map<String, Collection<Cluster>> regionToClusters, String region, boolean showDetails) {\n        Collection<Cluster> clusters = regionToClusters.get(region);\n        if (clusters == null) {\n            clusters = Lists.newArrayList();\n        }\n        message.append(String.format(\"Total %s clusters = %d in region %s<br/>\",\n                summaryName, clusters.size(), region));\n        if (showDetails) {\n            List<String> clusterNames = Lists.newArrayList();\n            for (Cluster cluster : clusters) {\n                clusterNames.add(cluster.getName());\n            }\n            message.append(String.format(\"List: %s<br/><br/>\", StringUtils.join(clusterNames, \",\")));\n        }\n    }\n\n    /**\n     * Gets the summary email subject for the last run of conformity monkey.\n     * @return the subject of the summary email\n     */\n    protected String getSummaryEmailSubject() {\n        return String.format(\"Conformity monkey execution summary (%s)\", StringUtils.join(regions, \",\"));\n    }\n\n    private boolean isConformityMonkeyEnabled() {\n        String prop = NS + \"enabled\";\n        if (cfg.getBoolOrElse(prop, true)) {\n            return true;\n        }\n        LOGGER.info(\"Conformity Monkey is disabled, set {}=true\", prop);\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkeyContext.java",
    "content": "/*\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE MagicNumberCheck\npackage com.netflix.simianarmy.basic.conformity;\n\nimport java.util.Collection;\nimport java.util.Map;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.amazonaws.regions.Region;\nimport com.amazonaws.regions.Regions;\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.inject.Guice;\nimport com.google.inject.Injector;\nimport com.netflix.discovery.DiscoveryClient;\nimport com.netflix.discovery.guice.EurekaModule;\nimport com.netflix.simianarmy.aws.conformity.RDSConformityClusterTracker;\nimport com.netflix.simianarmy.aws.conformity.SimpleDBConformityClusterTracker;\nimport com.netflix.simianarmy.aws.conformity.crawler.AWSClusterCrawler;\nimport com.netflix.simianarmy.aws.conformity.rule.BasicConformityEurekaClient;\nimport com.netflix.simianarmy.aws.conformity.rule.ConformityEurekaClient;\nimport com.netflix.simianarmy.aws.conformity.rule.CrossZoneLoadBalancing;\nimport com.netflix.simianarmy.aws.conformity.rule.InstanceHasHealthCheckUrl;\nimport com.netflix.simianarmy.aws.conformity.rule.InstanceHasStatusUrl;\nimport com.netflix.simianarmy.aws.conformity.rule.InstanceInSecurityGroup;\nimport com.netflix.simianarmy.aws.conformity.rule.InstanceInVPC;\nimport com.netflix.simianarmy.aws.conformity.rule.InstanceIsHealthyInEureka;\nimport com.netflix.simianarmy.aws.conformity.rule.InstanceTooOld;\nimport com.netflix.simianarmy.aws.conformity.rule.SameZonesInElbAndAsg;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.ClusterCrawler;\nimport com.netflix.simianarmy.conformity.ConformityClusterTracker;\nimport com.netflix.simianarmy.conformity.ConformityEmailBuilder;\nimport com.netflix.simianarmy.conformity.ConformityEmailNotifier;\nimport com.netflix.simianarmy.conformity.ConformityMonkey;\nimport com.netflix.simianarmy.conformity.ConformityRule;\nimport com.netflix.simianarmy.conformity.ConformityRuleEngine;\n\n/**\n * The basic implementation of the context class for Conformity monkey.\n */\npublic class BasicConformityMonkeyContext extends BasicSimianArmyContext implements ConformityMonkey.Context {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicConformityMonkeyContext.class);\n\n    /** The email notifier. */\n    private final ConformityEmailNotifier emailNotifier;\n\n    private final ConformityClusterTracker clusterTracker;\n\n    private final Collection<String> regions;\n\n    private final ClusterCrawler clusterCrawler;\n\n    private final AmazonSimpleEmailServiceClient sesClient;\n\n    private final ConformityEmailBuilder conformityEmailBuilder;\n\n    private final String defaultEmail;\n\n    private final String[] ccEmails;\n\n    private final String sourceEmail;\n\n    private final ConformityRuleEngine ruleEngine;\n\n    private final boolean leashed;\n\n    private final Map<String, AWSClient> regionToAwsClient = Maps.newHashMap();\n\n    /**\n     * The constructor.\n     */\n    public BasicConformityMonkeyContext() {\n        super(\"simianarmy.properties\", \"client.properties\", \"conformity.properties\");\n        regions = Lists.newArrayList(region());\n\n        // By default, the monkey is leashed\n        leashed = configuration().getBoolOrElse(\"simianarmy.conformity.leashed\", true);\n\n        LOGGER.info(String.format(\"Conformity Monkey is running in: %s\", regions));\n\n        String sdbDomain = configuration().getStrOrElse(\"simianarmy.conformity.sdb.domain\", \"SIMIAN_ARMY\");\n\n        String dbDriver = configuration().getStr(\"simianarmy.recorder.db.driver\");\n        String dbUser = configuration().getStr(\"simianarmy.recorder.db.user\");\n        String dbPass = configuration().getStr(\"simianarmy.recorder.db.pass\");\n        String dbUrl = configuration().getStr(\"simianarmy.recorder.db.url\");\n        String dbTable = configuration().getStr(\"simianarmy.conformity.resources.db.table\");\n        \n        if (dbDriver == null) {       \n        \tclusterTracker = new SimpleDBConformityClusterTracker(awsClient(), sdbDomain);\n        } else {\n        \tRDSConformityClusterTracker rdsClusterTracker = new RDSConformityClusterTracker(dbDriver, dbUser, dbPass, dbUrl, dbTable);\n        \trdsClusterTracker.init();\n        \tclusterTracker = rdsClusterTracker;\n        }\n\n        ruleEngine = new ConformityRuleEngine();\n        boolean eurekaEnabled = configuration().getBoolOrElse(\"simianarmy.conformity.Eureka.enabled\", false);\n\n        if (eurekaEnabled) {\n            LOGGER.info(\"Initializing Discovery client.\");\n            Injector injector = Guice.createInjector(new EurekaModule());\n            DiscoveryClient discoveryClient = injector.getInstance(DiscoveryClient.class);\n            ConformityEurekaClient conformityEurekaClient = new BasicConformityEurekaClient(discoveryClient);\n            if (configuration().getBoolOrElse(\n                    \"simianarmy.conformity.rule.InstanceIsHealthyInEureka.enabled\", false)) {\n                ruleEngine.addRule(new InstanceIsHealthyInEureka(conformityEurekaClient));\n            }\n            if (configuration().getBoolOrElse(\n                    \"simianarmy.conformity.rule.InstanceHasHealthCheckUrl.enabled\", false)) {\n                ruleEngine.addRule(new InstanceHasHealthCheckUrl(conformityEurekaClient));\n            }\n            if (configuration().getBoolOrElse(\n                    \"simianarmy.conformity.rule.InstanceHasStatusUrl.enabled\", false)) {\n                ruleEngine.addRule(new InstanceHasStatusUrl(conformityEurekaClient));\n            }\n        } else {\n            LOGGER.info(\"Discovery/Eureka is not enabled, the conformity rules that need Eureka are not added.\");\n        }\n\n        if (configuration().getBoolOrElse(\n                \"simianarmy.conformity.rule.InstanceInSecurityGroup.enabled\", false)) {\n            String requiredSecurityGroups = configuration().getStr(\n                    \"simianarmy.conformity.rule.InstanceInSecurityGroup.requiredSecurityGroups\");\n            if (!StringUtils.isBlank(requiredSecurityGroups)) {\n                ruleEngine.addRule(new InstanceInSecurityGroup(getAwsCredentialsProvider(),\n                        StringUtils.split(requiredSecurityGroups, \",\")));\n            } else {\n                LOGGER.info(\"No required security groups is specified, \"\n                        + \"the conformity rule InstanceInSecurityGroup is ignored.\");\n            }\n        }\n\n        if (configuration().getBoolOrElse(\n                \"simianarmy.conformity.rule.InstanceTooOld.enabled\", false)) {\n                ruleEngine.addRule(new InstanceTooOld(getAwsCredentialsProvider(), (int) configuration().getNumOrElse(\n                        \"simianarmy.conformity.rule.InstanceTooOld.instanceAgeThreshold\", 180)));\n        }\n\n        if (configuration().getBoolOrElse(\n                \"simianarmy.conformity.rule.SameZonesInElbAndAsg.enabled\", false)) {\n            ruleEngine().addRule(new SameZonesInElbAndAsg(getAwsCredentialsProvider()));\n        }\n\n        if (configuration().getBoolOrElse(\n                \"simianarmy.conformity.rule.InstanceInVPC.enabled\", false)) {\n                ruleEngine.addRule(new InstanceInVPC(getAwsCredentialsProvider()));\n        }\n\n        if (configuration().getBoolOrElse(\n                \"simianarmy.conformity.rule.CrossZoneLoadBalancing.enabled\", false)) {\n                ruleEngine().addRule(new CrossZoneLoadBalancing(getAwsCredentialsProvider()));\n        }\n        \n        createClient(region());\n        regionToAwsClient.put(region(), awsClient());\n\n        clusterCrawler = new AWSClusterCrawler(regionToAwsClient, configuration());\n        sesClient = new AmazonSimpleEmailServiceClient();\n        if (configuration().getStr(\"simianarmy.aws.email.region\") != null) {\n          sesClient.setRegion(Region.getRegion(Regions.fromName(configuration().getStr(\"simianarmy.aws.email.region\"))));\n        }        \n        defaultEmail = configuration().getStrOrElse(\"simianarmy.conformity.notification.defaultEmail\", null);\n        ccEmails = StringUtils.split(\n                configuration().getStrOrElse(\"simianarmy.conformity.notification.ccEmails\", \"\"), \",\");\n        sourceEmail = configuration().getStrOrElse(\"simianarmy.conformity.notification.sourceEmail\", null);\n        conformityEmailBuilder = new BasicConformityEmailBuilder();\n        emailNotifier = new ConformityEmailNotifier(getConformityEmailNotifierContext());\n    }\n\n    public ConformityEmailNotifier.Context getConformityEmailNotifierContext() {\n        return new ConformityEmailNotifier.Context() {\n            @Override\n            public AmazonSimpleEmailServiceClient sesClient() {\n                return sesClient;\n            }\n\n            @Override\n            public int openHour() {\n                return (int) configuration().getNumOrElse(\"simianarmy.conformity.notification.openHour\", 0);\n            }\n\n            @Override\n            public int closeHour() {\n                return (int) configuration().getNumOrElse(\"simianarmy.conformity.notification.closeHour\", 24);\n            }\n\n            @Override\n            public String defaultEmail() {\n                return defaultEmail;\n            }\n\n            @Override\n            public Collection<String> regions() {\n                return regions;\n            }\n\n            @Override\n            public ConformityClusterTracker clusterTracker() {\n                return clusterTracker;\n            }\n\n            @Override\n            public ConformityEmailBuilder emailBuilder() {\n                return conformityEmailBuilder;\n            }\n\n            @Override\n            public String[] ccEmails() {\n                return ccEmails;\n            }\n\n            @Override\n            public Collection<ConformityRule> rules() {\n                return ruleEngine.rules();\n            }\n\n            @Override\n            public String sourceEmail() {\n                return sourceEmail;\n            }\n        };\n    }\n\n    @Override\n    public ClusterCrawler clusterCrawler() {\n        return clusterCrawler;\n    }\n\n    @Override\n    public ConformityRuleEngine ruleEngine() {\n        return ruleEngine;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public ConformityEmailNotifier emailNotifier() {\n        return emailNotifier;\n    }\n\n    @Override\n    public Collection<String> regions() {\n        return regions;\n    }\n\n    @Override\n    public boolean isLeashed() {\n        return leashed;\n    }\n\n    @Override\n    public ConformityClusterTracker clusterTracker() {\n        return clusterTracker;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/janitor/BasicJanitorEmailBuilder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.janitor;\n\nimport java.util.Collection;\nimport java.util.Map;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.janitor.JanitorEmailBuilder;\n\n/** The basic implementation of the email builder for Janitor monkey. */\npublic class BasicJanitorEmailBuilder extends JanitorEmailBuilder {\n    private static final String[] TABLE_COLUMNS =\n        {\"Resource Type\", \"Resource\", \"Region\", \"Description\", \"Expected Termination Time\",\n        \"Termination Reason\", \"View/Edit\"};\n    private static final String AHREF_TEMPLATE = \"<a href=\\\"%s\\\">%s</a>\";\n    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern(\"EEE, MMM dd, yyyy\");\n\n    private Map<String, Collection<Resource>> emailToResources;\n\n    @Override\n    public void setEmailToResources(Map<String, Collection<Resource>> emailToResources) {\n        Validate.notNull(emailToResources);\n        this.emailToResources = emailToResources;\n    }\n\n    @Override\n    protected String getHeader() {\n        StringBuilder header = new StringBuilder();\n        header.append(\"<b><h2>Janitor Notifications</h2></b>\");\n        header.append(\n                \"The following resource(s) have been marked for cleanup by Janitor monkey \"\n                        + \"as potential unused resources. This is a non-repeating notification.<br/>\");\n        return header.toString();\n    }\n\n    @Override\n    protected String getEntryTable(String emailAddress) {\n        StringBuilder table = new StringBuilder();\n        table.append(getHtmlTableHeader(getTableColumns()));\n        for (Resource resource : emailToResources.get(emailAddress)) {\n            table.append(getResourceRow(resource));\n        }\n        table.append(\"</table>\");\n        return table.toString();\n    }\n\n    @Override\n    protected String getFooter() {\n        return \"<br/>Janitor Monkey wiki: https://github.com/Netflix/SimianArmy/wiki<br/>\";\n    }\n\n    /**\n     * Gets the url to view the details of the resource.\n     * @param resource the resource\n     * @return the url to view/edit the resource.\n     */\n    protected String getResourceUrl(Resource resource) {\n        return null;\n    }\n\n    /**\n     * Gets the string when displaying the resource, e.g. the id.\n     * @param resource the resource to display\n     * @return the string to represent the resource\n     */\n    protected String getResourceDisplay(Resource resource) {\n        return resource.getId();\n    }\n\n    /**\n     * Gets the url to edit the Janitor termination of the resource.\n     * @param resource the resource\n     * @return the url to edit the Janitor termination the resource.\n     */\n    protected String getJanitorResourceUrl(Resource resource) {\n        return null;\n    }\n\n    /** Gets the table columns for the table in the email.\n     *\n     * @return the array of column names\n     */\n    protected String[] getTableColumns() {\n        return TABLE_COLUMNS;\n    }\n\n    /**\n     * Gets the row for a resource in the table in the email body.\n     * @param resource the resource to display\n     * @return the table row in the email body\n     */\n    protected String getResourceRow(Resource resource) {\n        StringBuilder message = new StringBuilder();\n        message.append(\"<tr>\");\n        message.append(getHtmlCell(resource.getResourceType().name()));\n        String resourceUrl = getResourceUrl(resource);\n        if (!StringUtils.isEmpty(resourceUrl)) {\n            message.append(getHtmlCell(String.format(AHREF_TEMPLATE, resourceUrl, getResourceDisplay(resource))));\n        } else {\n            message.append(getHtmlCell(getResourceDisplay(resource)));\n        }\n        message.append(getHtmlCell(resource.getRegion()));\n        if (resource.getDescription() == null) {\n            message.append(getHtmlCell(\"\"));\n        } else {\n            message.append(getHtmlCell(resource.getDescription().replace(\";\", \"<br/>\").replace(\",\", \"<br/>\")));\n        }\n        message.append(getHtmlCell(DATE_FORMATTER.print(resource.getExpectedTerminationTime().getTime())));\n        message.append(getHtmlCell(resource.getTerminationReason()));\n        String janitorUrl = getJanitorResourceUrl(resource);\n        if (!StringUtils.isEmpty(janitorUrl)) {\n            message.append(getHtmlCell(String.format(AHREF_TEMPLATE, janitorUrl, \"View/Extend\")));\n        } else {\n            message.append(getHtmlCell(\"\"));\n        }\n        message.append(\"</tr>\");\n        return message.toString();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/janitor/BasicJanitorMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.janitor;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicLong;\nimport com.netflix.servo.annotations.DataSourceType;\nimport com.netflix.servo.annotations.Monitor;\nimport com.netflix.servo.monitor.Monitors;\nimport com.netflix.simianarmy.*;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.janitor.AbstractJanitor;\nimport com.netflix.simianarmy.janitor.JanitorEmailNotifier;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\nimport com.netflix.simianarmy.janitor.JanitorResourceTracker;\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/** The basic implementation of Janitor Monkey. */\npublic class BasicJanitorMonkey extends JanitorMonkey {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicJanitorMonkey.class);\n\n    /** The Constant NS. */\n    private static final String NS = \"simianarmy.janitor.\";\n\n    /** The cfg. */\n    private final MonkeyConfiguration cfg;\n\n    private final List<AbstractJanitor> janitors;\n\n    private final JanitorEmailNotifier emailNotifier;\n\n    private final String region;\n\n    private final String accountName;\n\n    private final JanitorResourceTracker resourceTracker;\n\n    private final MonkeyRecorder recorder;\n\n    private final MonkeyCalendar calendar;\n    \n    /** Keep track of the number of monkey runs */\n    protected final AtomicLong monkeyRuns = new AtomicLong(0);    \n\n    /** Keep track of the number of monkey errors */\n    protected final AtomicLong monkeyErrors = new AtomicLong(0);    \n    \n    /** Emit a servor signal to track the running monkey */\n    protected final AtomicLong monkeyRunning = new AtomicLong(0);    \n    \n    /**\n     * Instantiates a new basic janitor monkey.\n     *\n     * @param ctx\n     *            the ctx\n     */\n    public BasicJanitorMonkey(Context ctx) {\n        super(ctx);\n        this.cfg = ctx.configuration();\n\n        janitors = ctx.janitors();\n        emailNotifier = ctx.emailNotifier();\n        region = ctx.region();\n        accountName = ctx.accountName();\n        resourceTracker = ctx.resourceTracker();\n        recorder = ctx.recorder();\n        calendar = ctx.calendar();\n\n        // register this janitor with servo\n        Monitors.registerObject(\"simianarmy.janitor\", this);                \n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void doMonkeyBusiness() {\n        cfg.reload();\n        context().resetEventReport();\n\n        if (!isJanitorMonkeyEnabled()) {\n            return;\n        } else {\n            LOGGER.info(String.format(\"Marking resources with %d janitors.\", janitors.size()));\n            monkeyRuns.incrementAndGet();\n            monkeyRunning.set(1);\n            \n            // prepare to run, this just resets the counts so monitoring is sane\n            for (AbstractJanitor janitor : janitors) {\n            \tjanitor.prepareToRun();\n            }\n            \n            for (AbstractJanitor janitor : janitors) {\n                LOGGER.info(String.format(\"Running %s janitor for region %s\", janitor.getResourceType(), janitor.getRegion()));\n                try {\n                \tjanitor.markResources();\n                } catch (Exception e) {\n                \tmonkeyErrors.incrementAndGet();\n                \tLOGGER.error(String.format(\"Got an exception while %s janitor was marking for region %s\", janitor.getResourceType(), janitor.getRegion()), e);\n                }\n                LOGGER.info(String.format(\"Marked %d resources of type %s in the last run.\",\n                        janitor.getMarkedResources().size(), janitor.getResourceType().name()));\n                LOGGER.info(String.format(\"Unmarked %d resources of type %s in the last run.\",\n                        janitor.getUnmarkedResources().size(), janitor.getResourceType()));\n            }\n\n            if (!cfg.getBoolOrElse(\"simianarmy.janitor.leashed\", true)) {\n                emailNotifier.sendNotifications();\n            } else {\n                LOGGER.info(\"Janitor Monkey is leashed, no notification is sent.\");\n            }\n\n            LOGGER.info(String.format(\"Cleaning resources with %d janitors.\", janitors.size()));\n            for (AbstractJanitor janitor : janitors) {\n            \ttry {\n            \t\tjanitor.cleanupResources();\n                } catch (Exception e) {\n                \tmonkeyErrors.incrementAndGet();\n                \tLOGGER.error(String.format(\"Got an exception while %s janitor was cleaning for region %s\", janitor.getResourceType(), janitor.getRegion()), e);\n                }\n                LOGGER.info(String.format(\"Cleaned %d resources of type %s in the last run.\",\n                        janitor.getCleanedResources().size(), janitor.getResourceType()));\n                LOGGER.info(String.format(\"Failed to clean %d resources of type %s in the last run.\",\n                        janitor.getFailedToCleanResources().size(), janitor.getResourceType()));\n            }\n            if (cfg.getBoolOrElse(NS + \"summaryEmail.enabled\", true)) {\n                sendJanitorSummaryEmail();\n            }\n        \tmonkeyRunning.set(0);\n        }\n    }\n\n    @Override\n    public Event optInResource(String resourceId) {\n        return optInOrOutResource(resourceId, true, region);\n    }\n\n    @Override\n    public Event optOutResource(String resourceId) {\n        return optInOrOutResource(resourceId, false, region);\n    }\n\n    @Override\n    public Event optInResource(String resourceId, String resourceRegion) {\n        return optInOrOutResource(resourceId, true, resourceRegion);\n    }\n\n    @Override\n    public Event optOutResource(String resourceId, String resourceRegion) {\n        return optInOrOutResource(resourceId, false, resourceRegion);\n    }\n\n    private Event optInOrOutResource(String resourceId, boolean optIn, String resourceRegion) {\n        if (resourceRegion == null) {\n            resourceRegion = region;\n        }\n\n        Resource resource = resourceTracker.getResource(resourceId, resourceRegion);\n        if (resource == null) {\n            return null;\n        }\n\n        EventTypes eventType = optIn ? EventTypes.OPT_IN_RESOURCE : EventTypes.OPT_OUT_RESOURCE;\n        long timestamp = calendar.now().getTimeInMillis();\n        // The same resource can have multiple events, so we add the timestamp to the id.\n        Event evt = recorder.newEvent(Type.JANITOR, eventType, resource, resourceId + \"@\" + timestamp);\n        recorder.recordEvent(evt);\n        resource.setOptOutOfJanitor(!optIn);\n        resourceTracker.addOrUpdate(resource);\n        return evt;\n    }\n\n    /**\n     * Send a summary email with about the last run of the janitor monkey.\n     */\n    protected void sendJanitorSummaryEmail() {\n        String summaryEmailTarget = cfg.getStr(NS + \"summaryEmail.to\");\n        if (!StringUtils.isEmpty(summaryEmailTarget)) {\n            if (!emailNotifier.isValidEmail(summaryEmailTarget)) {\n                LOGGER.error(String.format(\"The email target address '%s' for Janitor summary email is invalid\",\n                        summaryEmailTarget));\n                return;\n            }\n            StringBuilder message = new StringBuilder();\n            for (AbstractJanitor janitor : janitors) {\n                ResourceType resourceType = janitor.getResourceType();\n                appendSummary(message, \"markings\", resourceType, janitor.getMarkedResources(), janitor.getRegion());\n                appendSummary(message, \"unmarkings\", resourceType, janitor.getUnmarkedResources(), janitor.getRegion());\n                appendSummary(message, \"cleanups\", resourceType, janitor.getCleanedResources(), janitor.getRegion());\n                appendSummary(message, \"cleanup failures\", resourceType, janitor.getFailedToCleanResources(),\n                        janitor.getRegion());\n            }\n            String subject = getSummaryEmailSubject();\n            emailNotifier.sendEmail(summaryEmailTarget, subject, message.toString());\n        }\n    }\n\n    private void appendSummary(StringBuilder message, String summaryName,\n            ResourceType resourceType, Collection<Resource> resources, String janitorRegion) {\n        message.append(String.format(\"Total %s for %s = %d in region %s<br/>\",\n                summaryName, resourceType.name(), resources.size(), janitorRegion));\n        message.append(String.format(\"List: %s<br/>\", printResources(resources)));\n    }\n\n    private String printResources(Collection<Resource> resources) {\n        StringBuilder sb = new StringBuilder();\n        boolean isFirst = true;\n        for (Resource r : resources) {\n            if (!isFirst) {\n                sb.append(\",\");\n            } else {\n                isFirst = false;\n            }\n            sb.append(r.getId());\n        }\n        return sb.toString();\n    }\n\n    /**\n     * Gets the summary email subject for the last run of janitor monkey.\n     * @return the subject of the summary email\n     */\n    protected String getSummaryEmailSubject() {\n        return String.format(\"Janitor monkey execution summary (%s, %s)\", accountName, region);\n    }\n\n    /**\n     * Handle cleanup error. This has been abstracted so subclasses can decide to continue causing chaos if desired.\n     *\n     * @param resource\n     *            the instance\n     * @param e\n     *            the exception\n     */\n    protected void handleCleanupError(Resource resource, Throwable e) {\n        String msg = String.format(\"Failed to clean up %s resource %s with error %s\",\n                resource.getResourceType(), resource.getId(), e.getMessage());\n        LOGGER.error(msg);\n        throw new RuntimeException(msg, e);\n    }\n\n    private boolean isJanitorMonkeyEnabled() {\n        String prop = NS + \"enabled\";\n        if (cfg.getBoolOrElse(prop, true)) {\n            return true;\n        }\n        LOGGER.info(\"JanitorMonkey disabled, set {}=true\", prop);\n        return false;\n    }\n    \n    @Monitor(name=\"runs\", type=DataSourceType.COUNTER)\n    public long getMonkeyRuns() {\n      return monkeyRuns.get();\n    }\n\n    @Monitor(name=\"errors\", type=DataSourceType.GAUGE)\n    public long getMonkeyErrors() {\n      return monkeyErrors.get();\n    }\n\n    @Monitor(name=\"running\", type=DataSourceType.GAUGE)\n    public long getMonkeyRunning() {\n      return monkeyRunning.get();\n    }\n    \n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/janitor/BasicJanitorMonkeyContext.java",
    "content": "/*\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE MagicNumberCheck\npackage com.netflix.simianarmy.basic.janitor;\n\nimport com.amazonaws.regions.Region;\nimport com.amazonaws.regions.Regions;\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.google.inject.Guice;\nimport com.google.inject.Injector;\nimport com.netflix.discovery.DiscoveryClient;\nimport com.netflix.discovery.guice.EurekaModule;\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.MonkeyRecorder;\nimport com.netflix.simianarmy.aws.janitor.*;\nimport com.netflix.simianarmy.aws.janitor.crawler.*;\nimport com.netflix.simianarmy.aws.janitor.crawler.edda.*;\nimport com.netflix.simianarmy.aws.janitor.rule.ami.UnusedImageRule;\nimport com.netflix.simianarmy.aws.janitor.rule.asg.*;\nimport com.netflix.simianarmy.aws.janitor.rule.elb.OrphanedELBRule;\nimport com.netflix.simianarmy.aws.janitor.rule.generic.TagValueExclusionRule;\nimport com.netflix.simianarmy.aws.janitor.rule.generic.UntaggedRule;\nimport com.netflix.simianarmy.aws.janitor.rule.instance.OrphanedInstanceRule;\nimport com.netflix.simianarmy.aws.janitor.rule.launchconfig.OldUnusedLaunchConfigRule;\nimport com.netflix.simianarmy.aws.janitor.rule.snapshot.NoGeneratedAMIRule;\nimport com.netflix.simianarmy.aws.janitor.rule.volume.DeleteOnTerminationRule;\nimport com.netflix.simianarmy.aws.janitor.rule.volume.OldDetachedVolumeRule;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.edda.EddaClient;\nimport com.netflix.simianarmy.janitor.*;\nimport org.apache.commons.lang.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * The basic implementation of the context class for Janitor monkey.\n */\npublic class BasicJanitorMonkeyContext extends BasicSimianArmyContext implements JanitorMonkey.Context {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicJanitorMonkeyContext.class);\n\n    /** The email notifier. */\n    private final JanitorEmailNotifier emailNotifier;\n\n    private final JanitorResourceTracker janitorResourceTracker;\n\n    /** The janitors. */\n    private final List<AbstractJanitor> janitors;\n\n    private final String monkeyRegion;\n\n    private final MonkeyCalendar monkeyCalendar;\n\n    private final AmazonSimpleEmailServiceClient sesClient;\n\n    private final JanitorEmailBuilder janitorEmailBuilder;\n\n    private final String defaultEmail;\n\n    private final String[] ccEmails;\n\n    private final String sourceEmail;\n\n    private final String ownerEmailDomain;\n\n    private final int daysBeforeTermination;\n\n    /**\n     * The constructor.\n     */\n    public BasicJanitorMonkeyContext() {\n        super(\"simianarmy.properties\", \"client.properties\", \"janitor.properties\");\n\n        monkeyRegion = region();\n        monkeyCalendar = calendar();\n\n        String resourceDomain = configuration().getStrOrElse(\"simianarmy.janitor.resources.sdb.domain\", \"SIMIAN_ARMY\");\n\n        Set<String> enabledResourceSet = getEnabledResourceSet();\n\n        String dbDriver = configuration().getStr(\"simianarmy.recorder.db.driver\");\n        String dbUser = configuration().getStr(\"simianarmy.recorder.db.user\");\n        String dbPass = configuration().getStr(\"simianarmy.recorder.db.pass\");\n        String dbUrl = configuration().getStr(\"simianarmy.recorder.db.url\");\n        String dbTable = configuration().getStr(\"simianarmy.janitor.resources.db.table\");\n        \n        if (dbDriver == null) {       \n        \tjanitorResourceTracker = new SimpleDBJanitorResourceTracker(awsClient(), resourceDomain);\n        } else {\n        \tRDSJanitorResourceTracker rdsTracker = new RDSJanitorResourceTracker(dbDriver, dbUser, dbPass, dbUrl, dbTable);\n        \trdsTracker.init();\n        \tjanitorResourceTracker = rdsTracker;\n        }\n\n        janitorEmailBuilder = new BasicJanitorEmailBuilder();\n        sesClient = new AmazonSimpleEmailServiceClient();\n        if (configuration().getStr(\"simianarmy.aws.email.region\") != null) {\n           sesClient.setRegion(Region.getRegion(Regions.fromName(configuration().getStr(\"simianarmy.aws.email.region\"))));\n        }\n        defaultEmail = configuration().getStrOrElse(\"simianarmy.janitor.notification.defaultEmail\", \"\");\n        ccEmails = StringUtils.split(\n                configuration().getStrOrElse(\"simianarmy.janitor.notification.ccEmails\", \"\"), \",\");\n        sourceEmail = configuration().getStrOrElse(\"simianarmy.janitor.notification.sourceEmail\", \"\");\n        ownerEmailDomain = configuration().getStrOrElse(\"simianarmy.janitor.notification.ownerEmailDomain\", \"\");\n        daysBeforeTermination =\n                (int) configuration().getNumOrElse(\"simianarmy.janitor.notification.daysBeforeTermination\", 3);\n\n        emailNotifier = new JanitorEmailNotifier(getJanitorEmailNotifierContext());\n\n        janitors = new ArrayList<AbstractJanitor>();\n        if (enabledResourceSet.contains(\"ASG\")) {\n            janitors.add(getASGJanitor());\n        }\n\n        if (enabledResourceSet.contains(\"INSTANCE\")) {\n            janitors.add(getInstanceJanitor());\n        }\n\n        if (enabledResourceSet.contains(\"EBS_VOLUME\")) {\n            janitors.add(getEBSVolumeJanitor());\n        }\n\n        if (enabledResourceSet.contains(\"EBS_SNAPSHOT\")) {\n            janitors.add(getEBSSnapshotJanitor());\n        }\n\n        if (enabledResourceSet.contains(\"LAUNCH_CONFIG\")) {\n            janitors.add(getLaunchConfigJanitor());\n        }\n\n        if (enabledResourceSet.contains(\"IMAGE\")) {\n            janitors.add(getImageJanitor());\n        }\n\n        if (enabledResourceSet.contains(\"ELB\")) {\n            janitors.add(getELBJanitor());\n        }\n\n    }\n    protected JanitorRuleEngine createJanitorRuleEngine() {\n        JanitorRuleEngine ruleEngine = new BasicJanitorRuleEngine();\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.TagValueExclusionRule.enabled\", false)) {\n            String tagsList = configuration().getStr(\"simianarmy.janitor.rule.TagValueExclusionRule.tags\");\n            String valsList = configuration().getStr(\"simianarmy.janitor.rule.TagValueExclusionRule.vals\");\n            if (tagsList != null && valsList != null) {\n                TagValueExclusionRule rule = new TagValueExclusionRule(tagsList.split(\",\"), valsList.split(\",\"));\n                ruleEngine.addExclusionRule(rule);\n            }\n        }\n        return ruleEngine;\n    }\n\n    private ASGJanitor getASGJanitor() {\n        JanitorRuleEngine ruleEngine = createJanitorRuleEngine();\n        boolean discoveryEnabled = configuration().getBoolOrElse(\"simianarmy.janitor.Eureka.enabled\", false);\n        ASGInstanceValidator instanceValidator;\n        if (discoveryEnabled) {\n            LOGGER.info(\"Initializing Discovery client.\");\n            Injector injector = Guice.createInjector(new EurekaModule());\n            DiscoveryClient discoveryClient = injector.getInstance(DiscoveryClient.class);\n            instanceValidator = new DiscoveryASGInstanceValidator(discoveryClient);\n        } else {\n            LOGGER.info(\"Discovery/Eureka is not enabled, use the dummy instance validator.\");\n            instanceValidator = new DummyASGInstanceValidator();\n        }\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.oldEmptyASGRule.enabled\", false)) {\n            ruleEngine.addRule(new OldEmptyASGRule(monkeyCalendar,\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.oldEmptyASGRule.launchConfigAgeThreshold\", 50),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.oldEmptyASGRule.retentionDays\", 10),\n                                    instanceValidator\n                    ));\n        }\n\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.suspendedASGRule.enabled\", false)) {\n            ruleEngine.addRule(new SuspendedASGRule(monkeyCalendar,\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.suspendedASGRule.suspensionAgeThreshold\", 2),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.suspendedASGRule.retentionDays\", 5),\n                                    instanceValidator\n                    ));\n        }\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.untaggedRule.enabled\", false)\n            && getUntaggedRuleResourceSet().contains(\"ASG\")) {\n            ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet(\"simianarmy.janitor.rule.untaggedRule.requiredTags\"),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner\", 3),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner\",\n                                    8)));\n        }\n\n        JanitorCrawler crawler;\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.edda.enabled\", false)) {\n            crawler = new EddaASGJanitorCrawler(createEddaClient(), awsClient().region());\n        } else {\n            crawler = new ASGJanitorCrawler(awsClient());\n        }\n        BasicJanitorContext asgJanitorCtx = new BasicJanitorContext(\n                monkeyRegion, ruleEngine, crawler, janitorResourceTracker,\n                monkeyCalendar, configuration(), recorder());\n        return new ASGJanitor(awsClient(), asgJanitorCtx);\n    }\n\n    private InstanceJanitor getInstanceJanitor() {\n        JanitorRuleEngine ruleEngine = createJanitorRuleEngine();\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.orphanedInstanceRule.enabled\", false)) {\n            ruleEngine.addRule(new OrphanedInstanceRule(monkeyCalendar,\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.orphanedInstanceRule.instanceAgeThreshold\", 2),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.orphanedInstanceRule.retentionDaysWithOwner\", 3),\n                                    (int) configuration().getNumOrElse(\n                                            \"simianarmy.janitor.rule.orphanedInstanceRule.retentionDaysWithoutOwner\",\n                                            8),\n                                    configuration().getBoolOrElse(\n                                            \"simianarmy.janitor.rule.orphanedInstanceRule.opsworks.parentage\",\n                                            false)));\n        }\n        \n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.untaggedRule.enabled\", false)\n            && getUntaggedRuleResourceSet().contains(\"INSTANCE\")) {\n            ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet(\"simianarmy.janitor.rule.untaggedRule.requiredTags\"),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner\", 3),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner\",\n                                    8)));\n        }\n\n        JanitorCrawler instanceCrawler;\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.edda.enabled\", false)) {\n            instanceCrawler = new EddaInstanceJanitorCrawler(createEddaClient(), awsClient().region());\n        } else {\n            instanceCrawler = new InstanceJanitorCrawler(awsClient());\n        }\n        BasicJanitorContext instanceJanitorCtx = new BasicJanitorContext(\n                monkeyRegion, ruleEngine, instanceCrawler, janitorResourceTracker,\n                monkeyCalendar, configuration(), recorder());\n        return new InstanceJanitor(awsClient(), instanceJanitorCtx);\n    }\n\n    private EBSVolumeJanitor getEBSVolumeJanitor() {\n        JanitorRuleEngine ruleEngine = createJanitorRuleEngine();\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.oldDetachedVolumeRule.enabled\", false)) {\n            ruleEngine.addRule(new OldDetachedVolumeRule(monkeyCalendar,\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.oldDetachedVolumeRule.detachDaysThreshold\", 30),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.oldDetachedVolumeRule.retentionDays\", 7)));\n\n            if (configuration().getBoolOrElse(\"simianarmy.janitor.edda.enabled\", false)\n                && configuration().getBoolOrElse(\"simianarmy.janitor.rule.deleteOnTerminationRule.enabled\", false)) {\n                ruleEngine.addRule(new DeleteOnTerminationRule(monkeyCalendar, (int) configuration().getNumOrElse(\n                        \"simianarmy.janitor.rule.deleteOnTerminationRule.retentionDays\", 3)));\n            }\n        }\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.untaggedRule.enabled\", false)\n            && getUntaggedRuleResourceSet().contains(\"EBS_VOLUME\")) {\n            ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet(\"simianarmy.janitor.rule.untaggedRule.requiredTags\"),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner\", 3),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner\",\n                                    8)));\n        }\n\n        JanitorCrawler volumeCrawler;\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.edda.enabled\", false)) {\n            volumeCrawler = new EddaEBSVolumeJanitorCrawler(createEddaClient(), awsClient().region());\n        } else {\n            volumeCrawler = new EBSVolumeJanitorCrawler(awsClient());\n        }\n\n        BasicJanitorContext volumeJanitorCtx = new BasicJanitorContext(\n                monkeyRegion, ruleEngine, volumeCrawler, janitorResourceTracker,\n                monkeyCalendar, configuration(), recorder());\n        return new EBSVolumeJanitor(awsClient(), volumeJanitorCtx);\n    }\n\n    private EBSSnapshotJanitor getEBSSnapshotJanitor() {\n        JanitorRuleEngine ruleEngine = createJanitorRuleEngine();\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.noGeneratedAMIRule.enabled\", false)) {\n            ruleEngine.addRule(new NoGeneratedAMIRule(monkeyCalendar,\n                    (int) configuration().getNumOrElse(\"simianarmy.janitor.rule.noGeneratedAMIRule.ageThreshold\", 30),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.noGeneratedAMIRule.retentionDays\", 7),\n                    configuration().getStrOrElse(\n                            \"simianarmy.janitor.rule.noGeneratedAMIRule.ownerEmail\", null)));\n        }\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.untaggedRule.enabled\", false)\n            && getUntaggedRuleResourceSet().contains(\"EBS_SNAPSHOT\")) {\n            ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet(\"simianarmy.janitor.rule.untaggedRule.requiredTags\"),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner\", 3),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner\",\n                                    8)));\n        }\n\n        JanitorCrawler snapshotCrawler;\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.edda.enabled\", false)) {\n            snapshotCrawler = new EddaEBSSnapshotJanitorCrawler(\n                    configuration().getStr(\"simianarmy.janitor.snapshots.ownerId\"),\n                    createEddaClient(), awsClient().region());\n        } else {\n            snapshotCrawler = new EBSSnapshotJanitorCrawler(awsClient());\n        }\n        BasicJanitorContext snapshotJanitorCtx = new BasicJanitorContext(\n                monkeyRegion, ruleEngine, snapshotCrawler, janitorResourceTracker,\n                monkeyCalendar, configuration(), recorder());\n        return new EBSSnapshotJanitor(awsClient(), snapshotJanitorCtx);\n    }\n\n    private LaunchConfigJanitor getLaunchConfigJanitor() {\n        JanitorRuleEngine ruleEngine = createJanitorRuleEngine();\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.oldUnusedLaunchConfigRule.enabled\", false)) {\n            ruleEngine.addRule(new OldUnusedLaunchConfigRule(monkeyCalendar,\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.oldUnusedLaunchConfigRule.ageThreshold\", 4),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.oldUnusedLaunchConfigRule.retentionDays\", 3)));\n        }\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.untaggedRule.enabled\", false)\n            && getUntaggedRuleResourceSet().contains(\"LAUNCH_CONFIG\")) {\n            ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet(\"simianarmy.janitor.rule.untaggedRule.requiredTags\"),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner\", 3),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner\",\n                                    8)));\n        }\n\n        JanitorCrawler crawler;\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.edda.enabled\", false)) {\n            crawler = new EddaLaunchConfigJanitorCrawler(\n                    createEddaClient(), awsClient().region());\n        } else {\n            crawler = new LaunchConfigJanitorCrawler(awsClient());\n        }\n        BasicJanitorContext janitorCtx = new BasicJanitorContext(\n                monkeyRegion, ruleEngine, crawler, janitorResourceTracker,\n                monkeyCalendar, configuration(), recorder());\n        return new LaunchConfigJanitor(awsClient(), janitorCtx);\n    }\n\n    private ImageJanitor getImageJanitor() {\n        JanitorCrawler crawler;\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.edda.enabled\", false)) {\n            crawler = new EddaImageJanitorCrawler(createEddaClient(),\n                    configuration().getStr(\"simianarmy.janitor.image.ownerId\"),\n                    (int) configuration().getNumOrElse(\"simianarmy.janitor.image.crawler.lookBackDays\", 60),\n                    awsClient().region());\n        } else {\n            throw new RuntimeException(\"Image Janitor only works when Edda is enabled.\");\n        }\n\n        JanitorRuleEngine ruleEngine = createJanitorRuleEngine();\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.unusedImageRule.enabled\", false)) {\n            ruleEngine.addRule(new UnusedImageRule(monkeyCalendar,\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.unusedImageRule.retentionDays\", 3),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.unusedImageRule.lastReferenceDaysThreshold\", 45)));\n        }\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.untaggedRule.enabled\", false)\n            && getUntaggedRuleResourceSet().contains(\"IMAGE\")) {\n            ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet(\"simianarmy.janitor.rule.untaggedRule.requiredTags\"),\n                    (int) configuration().getNumOrElse(\n                            \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner\", 3),\n                            (int) configuration().getNumOrElse(\n                                    \"simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner\",\n                                    8)));\n        }\n\n        BasicJanitorContext janitorCtx = new BasicJanitorContext(\n                monkeyRegion, ruleEngine, crawler, janitorResourceTracker,\n                monkeyCalendar, configuration(), recorder());\n        return new ImageJanitor(awsClient(), janitorCtx);\n    }\n\n\n    private ELBJanitor getELBJanitor() {\n        JanitorRuleEngine ruleEngine = createJanitorRuleEngine();\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.orphanedELBRule.enabled\", false)) {\n            ruleEngine.addRule(new OrphanedELBRule(monkeyCalendar,\n                (int) configuration().getNumOrElse(\n                        \"simianarmy.janitor.rule.orphanedELBRule.retentionDays\", 7)));\n        }\n\n        JanitorCrawler elbCrawler;\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.edda.enabled\", false)) {\n            boolean useEddaApplicationOwner = configuration().getBoolOrElse(\"simianarmy.janitor.rule.orphanedELBRule.edda.useApplicationOwner\", false);\n            String eddaFallbackOwnerEmail = configuration().getStr(\"simianarmy.janitor.rule.orphanedELBRule.edda.fallbackOwnerEmail\");\n            elbCrawler = new EddaELBJanitorCrawler(createEddaClient(), eddaFallbackOwnerEmail, useEddaApplicationOwner, awsClient().region());\n        } else {\n            elbCrawler = new ELBJanitorCrawler(awsClient());\n        }\n        BasicJanitorContext elbJanitorCtx = new BasicJanitorContext(\n                monkeyRegion, ruleEngine, elbCrawler, janitorResourceTracker,\n                monkeyCalendar, configuration(), recorder());\n        return new ELBJanitor(awsClient(), elbJanitorCtx);\n    }\n\n    private EddaClient createEddaClient() {\n        return new EddaClient((int) configuration().getNumOrElse(\"simianarmy.janitor.edda.client.timeout\", 30000),\n                (int) configuration().getNumOrElse(\"simianarmy.janitor.edda.client.retries\", 3),\n                (int) configuration().getNumOrElse(\"simianarmy.janitor.edda.client.retryInterval\", 1000),\n                configuration());\n    }\n\n    private Set<String> getEnabledResourceSet() {\n        Set<String> enabledResourceSet = new HashSet<String>();\n        String enabledResources = configuration().getStr(\"simianarmy.janitor.enabledResources\");\n        if (StringUtils.isNotBlank(enabledResources)) {\n            for (String resourceType : enabledResources.split(\",\")) {\n                enabledResourceSet.add(resourceType.trim().toUpperCase());\n            }\n        }\n        return enabledResourceSet;\n    }\n\n    private Set<String> getUntaggedRuleResourceSet() {\n        Set<String> untaggedRuleResourceSet = new HashSet<String>();\n        if (configuration().getBoolOrElse(\"simianarmy.janitor.rule.untaggedRule.enabled\", false)) {\n            String untaggedRuleResources = configuration().getStr(\"simianarmy.janitor.rule.untaggedRule.resources\");\n            if (StringUtils.isNotBlank(untaggedRuleResources)) {\n                for (String resourceType : untaggedRuleResources.split(\",\")) {\n                    untaggedRuleResourceSet.add(resourceType.trim().toUpperCase());\n                }\n            }\n        }\n        return untaggedRuleResourceSet;\n    }\n \n    private Set<String> getPropertySet(String property) {\n        Set<String> propertyValueSet = new HashSet<String>();\n        String propertyValue = configuration().getStr(property);\n        if (StringUtils.isNotBlank(propertyValue)) {\n            for (String propertyValueItem : propertyValue.split(\",\")) {\n                propertyValueSet.add(propertyValueItem.trim());\n            }\n        }\n        return propertyValueSet;\n    }\n\n    public JanitorEmailNotifier.Context getJanitorEmailNotifierContext() {\n        return new JanitorEmailNotifier.Context() {\n            @Override\n            public AmazonSimpleEmailServiceClient sesClient() {\n                return sesClient;\n            }\n\n            @Override\n            public String defaultEmail() {\n                return defaultEmail;\n            }\n\n            @Override\n            public int daysBeforeTermination() {\n                return daysBeforeTermination;\n            }\n\n            @Override\n            public String region() {\n                return monkeyRegion;\n            }\n\n            @Override\n            public JanitorResourceTracker resourceTracker() {\n                return janitorResourceTracker;\n            }\n\n            @Override\n            public JanitorEmailBuilder emailBuilder() {\n                return janitorEmailBuilder;\n            }\n\n            @Override\n            public MonkeyCalendar calendar() {\n                return monkeyCalendar;\n            }\n\n            @Override\n            public String[] ccEmails() {\n                return ccEmails;\n            }\n\n            @Override\n            public String sourceEmail() {\n                return sourceEmail;\n            }\n\n            @Override\n            public String ownerEmailDomain() {\n                return ownerEmailDomain;\n            }\n        };\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<AbstractJanitor> janitors() {\n        return janitors;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public JanitorEmailNotifier emailNotifier() {\n        return emailNotifier;\n    }\n\n    @Override\n    public JanitorResourceTracker resourceTracker() {\n        return janitorResourceTracker;\n    }\n\n    /** The Context class for Janitor.\n     */\n    public static class BasicJanitorContext implements AbstractJanitor.Context {\n        private final String region;\n        private final JanitorRuleEngine ruleEngine;\n        private final JanitorCrawler crawler;\n        private final JanitorResourceTracker resourceTracker;\n        private final MonkeyCalendar calendar;\n        private final MonkeyConfiguration config;\n        private final MonkeyRecorder recorder;\n\n        /**\n         * Constructor.\n         * @param region the region of the janitor\n         * @param ruleEngine the rule engine used by the janitor\n         * @param crawler the crawler used by the janitor\n         * @param resourceTracker the resource tracker used by the janitor\n         * @param calendar the calendar used by the janitor\n         * @param config the monkey configuration used by the janitor\n         */\n        public BasicJanitorContext(String region, JanitorRuleEngine ruleEngine, JanitorCrawler crawler,\n                JanitorResourceTracker resourceTracker, MonkeyCalendar calendar, MonkeyConfiguration config,\n                MonkeyRecorder recorder) {\n            this.region = region;\n            this.resourceTracker = resourceTracker;\n            this.ruleEngine = ruleEngine;\n            this.crawler = crawler;\n            this.calendar = calendar;\n            this.config = config;\n            this.recorder = recorder;\n        }\n\n        @Override\n        public String region() {\n            return region;\n        }\n\n        @Override\n        public MonkeyConfiguration configuration() {\n            return config;\n        }\n\n        @Override\n        public MonkeyCalendar calendar() {\n            return calendar;\n        }\n\n        @Override\n        public JanitorRuleEngine janitorRuleEngine() {\n            return ruleEngine;\n        }\n\n        @Override\n        public JanitorCrawler janitorCrawler() {\n            return crawler;\n        }\n\n        @Override\n        public JanitorResourceTracker janitorResourceTracker() {\n            return resourceTracker;\n        }\n\n        @Override\n        public MonkeyRecorder recorder() {\n            return recorder;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/janitor/BasicJanitorRuleEngine.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.basic.janitor;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.janitor.JanitorRuleEngine;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * Basic implementation of janitor rule engine that runs all containing rules to decide if a resource should be\n * a candidate of cleanup.\n */\npublic class BasicJanitorRuleEngine implements JanitorRuleEngine {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicJanitorRuleEngine.class);\n\n    /** The rules to decide if a resource should be a candidate for cleanup. **/\n    private final List<Rule> rules;\n\n    /** The rules to decide if a resource should be excluded for cleanup. **/\n    private final List<Rule> exclusionRules;\n\n    /**\n     * The constructor of JanitorRuleEngine.\n     */\n    public BasicJanitorRuleEngine() {\n        rules = new ArrayList<Rule>();\n        exclusionRules = new ArrayList<Rule>();\n    }\n\n    /**\n     * Decides whether the resource should be a candidate of cleanup based on the underlying rules. If any rule in the\n     * rule set thinks the resource should be a candidate of cleanup, the method returns false which indicates that the\n     * resource should be marked for cleanup. If multiple rules think the resource should be cleaned up, the rule with\n     * the nearest expected termination time fills the termination reason and expected termination time.\n     *\n     * @param resource\n     *            The resource\n     * @return true if the resource is valid and should not be a candidate of cleanup based on the underlying rules,\n     *         false otherwise.\n     */\n    @Override\n    public boolean isValid(Resource resource) {\n        LOGGER.debug(String.format(\"Checking if resource %s of type %s is a cleanup candidate against %d rules and %d exclusion rules.\",\n                resource.getId(), resource.getResourceType(), rules.size(), exclusionRules.size()));\n\n        for (Rule exclusionRule : exclusionRules) {\n            if (exclusionRule.isValid(resource)) {\n                LOGGER.info(String.format(\"Resource %s is not marked as a cleanup candidate because of an exclusion rule.\", resource.getId()));\n                return true;\n            }\n        }\n\n        // We create a clone of the resource each time when we try the rule. In the first iteration of the rules\n        // we identify the rule with the nearest termination date if there is any rule considers the resource\n        // as a cleanup candidate. Then the rule is applied to the original resource.\n        Rule nearestRule = null;\n        if (rules.size() == 1) {\n            nearestRule = rules.get(0);\n        } else {\n            Date nearestTerminationTime = null;\n            for (Rule rule : rules) {\n                Resource clone = resource.cloneResource();\n                if (!rule.isValid(clone)) {\n                    if (clone.getExpectedTerminationTime() != null) {\n                        if (nearestTerminationTime == null || nearestTerminationTime.after(clone.getExpectedTerminationTime())) {\n                            nearestRule = rule;\n                            nearestTerminationTime = clone.getExpectedTerminationTime();\n                        }\n                    }\n                }\n            }\n        }\n        if (nearestRule != null && !nearestRule.isValid(resource)) {\n            LOGGER.info(String.format(\"Resource %s is marked as a cleanup candidate.\", resource.getId()));\n            return false;\n        } else {\n            LOGGER.info(String.format(\"Resource %s is not marked as a cleanup candidate.\", resource.getId()));\n            return true;\n        }\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public BasicJanitorRuleEngine addRule(Rule rule) {\n        rules.add(rule);\n        return this;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public BasicJanitorRuleEngine addExclusionRule(Rule rule){\n        exclusionRules.add(rule);\n        return this;\n    }\n\n   /** {@inheritDoc} */\n    @Override\n    public List<Rule> getRules() {\n        return this.rules;\n    }\n\n\n    /** {@inheritDoc} */\n    @Override\n    public List<Rule> getExclusionRules() {\n        return this.exclusionRules;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/basic/janitor/BasicVolumeTaggingMonkeyContext.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.basic.janitor;\n\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey;\nimport com.netflix.simianarmy.basic.BasicSimianArmyContext;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport org.apache.commons.lang.StringUtils;\n\nimport java.util.Collection;\n\n/** The basic context for the monkey that tags volumes with Janitor meta data.\n */\npublic class BasicVolumeTaggingMonkeyContext extends BasicSimianArmyContext implements VolumeTaggingMonkey.Context {\n\n    private final Collection<AWSClient> awsClients = Lists.newArrayList();\n\n    /**\n     * The constructor.\n     */\n    public BasicVolumeTaggingMonkeyContext() {\n        super(\"simianarmy.properties\", \"client.properties\", \"volumeTagging.properties\");\n        for (String r : StringUtils.split(region(), \",\")) {\n            createClient(r);\n            awsClients.add(awsClient());\n        }\n    }\n\n    @Override\n    public Collection<AWSClient> awsClients() {\n        return awsClients;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/BlockAllNetworkTrafficChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport java.util.List;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.CloudClient;\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Blocks network traffic to/from instance, so it is running but offline.\n *\n * We actually put the instance into a different security group. First, because AWS requires a SG for some reason.\n * Second, because you might well want to continue to allow e.g. SSH inbound.\n */\npublic class BlockAllNetworkTrafficChaosType extends ChaosType {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BlockAllNetworkTrafficChaosType.class);\n\n    private final String blockedSecurityGroupName;\n\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public BlockAllNetworkTrafficChaosType(MonkeyConfiguration config) {\n        super(config, \"BlockAllNetworkTraffic\");\n\n        this.blockedSecurityGroupName = config.getStrOrElse(getConfigurationPrefix() + \"group\", \"blocked-network\");\n    }\n\n    /**\n     * We can apply the strategy iff the blocked security group is configured.\n     */\n    @Override\n    public boolean canApply(ChaosInstance instance) {\n        CloudClient cloudClient = instance.getCloudClient();\n        String instanceId = instance.getInstanceId();\n\n        if (!cloudClient.canChangeInstanceSecurityGroups(instanceId)) {\n            LOGGER.info(\"Not a VPC instance, can't change security groups\");\n            return false;\n        }\n\n        return super.canApply(instance);\n    }\n\n    /**\n     * Takes the instance off the network.\n     */\n    @Override\n    public void apply(ChaosInstance instance) {\n        CloudClient cloudClient = instance.getCloudClient();\n        String instanceId = instance.getInstanceId();\n\n        if (!cloudClient.canChangeInstanceSecurityGroups(instanceId)) {\n            throw new IllegalStateException(\"canApply should have returned false\");\n        }\n\n        String groupId = cloudClient.findSecurityGroup(instance.getInstanceId(), blockedSecurityGroupName);\n\n        if (groupId == null) {\n            LOGGER.info(\"Auto-creating security group {}\", blockedSecurityGroupName);\n\n            String description = \"Empty security group for blocked instances\";\n            groupId = cloudClient.createSecurityGroup(instance.getInstanceId(), blockedSecurityGroupName, description);\n        }\n\n        LOGGER.info(\"Blocking network traffic by applying security group {} to instance {}\", groupId, instanceId);\n\n        List<String> groups = Lists.newArrayList();\n        groups.add(groupId);\n        cloudClient.setInstanceSecurityGroups(instanceId, groups);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/BurnCpuChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Executes a CPU intensive program on the node, using up all available CPU.\n *\n * This simulates either a noisy CPU neighbor on the box or just a general issue with the CPU.\n */\npublic class BurnCpuChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public BurnCpuChaosType(MonkeyConfiguration config) {\n        super(config, \"BurnCpu\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/BurnIoChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Executes a disk I/O intensive program on the node, reducing I/O capacity.\n *\n * This simulates either a noisy neighbor on the box or just a general issue with the disk.\n */\npublic class BurnIoChaosType extends ScriptChaosType {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BurnIoChaosType.class);\n\n    /**\n     * Enhancement: It would be nice to target other devices than the root disk.\n     *\n     * Considerations:\n     * 1) EBS activity costs money.\n     * 2) The root may be on EBS anyway.\n     * 3) If it's costing money, we might want to stop after a while to stop runaway charges.\n     *\n     * coryb suggested this, and proposed something like this:\n     *\n     * tmp=$(mktemp)\n     * df -hl -x tmpfs | awk '/\\//{print $6}' > $tmp\n     * mount=$(sed -n $((RANDOM%$(wc -l < $tmp)+1))p $tmp)\n     * rm $tmp\n     *\n     * And then of=$mount/burn\n     *\n     * An alternative might be to run df over SSH, parse it here, and then pass the desired\n     * path to the script.  This keeps the script simpler.  I don't think there's an easy way\n     * to tell the difference between an EBS volume and an instance volume other than from the\n     * EC2 API.\n     */\n\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public BurnIoChaosType(MonkeyConfiguration config) {\n        super(config, \"BurnIO\");\n    }\n\n    @Override\n    public boolean canApply(ChaosInstance instance) {\n        if (!super.canApply(instance)) {\n            return false;\n        }\n\n        if (isRootVolumeEbs(instance) && !isBurnMoneyEnabled()) {\n            LOGGER.debug(\"Root volume is EBS so BurnIO would cost money; skipping\");\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/ChaosCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport java.util.EnumSet;\nimport java.util.List;\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.netflix.simianarmy.GroupType;\n\n/**\n * The Interface ChaosCrawler.\n */\npublic interface ChaosCrawler {\n\n    /**\n     * The Interface InstanceGroup.\n     */\n    public interface InstanceGroup {\n\n        /**\n         * Type.\n         *\n         * @return the group type enum\n         */\n        GroupType type();\n\n        /**\n         * Name.\n         *\n         * @return the group string\n         */\n        String name();\n\n        /**\n         * Region.\n         *\n         * @return the region the group exists in\n         */\n        String region();\n\n        /**\n         * Tags.\n         *\n         * @return the list of tags associated with group type\n         */\n        List<TagDescription> tags();\n\n        /**\n         * Instances.\n         *\n         * @return the list of instances\n         */\n        List<String> instances();\n\n        /**\n         * Adds the instance.\n         *\n         * @param instance\n         *            the instance\n         */\n        void addInstance(String instance);\n\n        /**\n         * Copies the Instance group replacing its name with\n         * the supplied name.\n         *\n         *\n         * @param name\n         * @return the new instance group\n         */\n        InstanceGroup copyAs(String name);\n    }\n\n    /**\n     * Group types.\n     *\n     * @return the type of groups this crawler creates \\set\n     */\n    EnumSet<?> groupTypes();\n\n    /**\n     * Groups.\n     *\n     * @return the list\n     */\n    List<InstanceGroup> groups();\n\n    /**\n     * Gets the up to date information for a collection of group names.\n     *\n     * @param names\n     *          the group names\n     * @return the list of instance groups\n     */\n    List<InstanceGroup> groups(String... names);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/ChaosEmailNotifier.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.netflix.simianarmy.aws.AWSEmailNotifier;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\n\n/** The email notifier for Chaos monkey.\n *\n */\npublic abstract class ChaosEmailNotifier extends AWSEmailNotifier {\n\n    /** Constructor. Currently the notifier is fixed the email client to\n     * Amazon Simple Email Service. We can release this restriction when\n     * we want to support different email clients.\n     *\n     * @param sesClient the AWS simple email service client.\n     */\n    public ChaosEmailNotifier(AmazonSimpleEmailServiceClient sesClient) {\n        super(sesClient);\n    }\n\n    /**\n     * Sends an email notification for a termination of instance to group\n     * owner's email address.\n     * @param group the instance group\n     * @param instance the instance id\n     * @param chaosType the chosen chaos strategy\n     */\n    public abstract void sendTerminationNotification(InstanceGroup group, String instance, ChaosType chaosType);\n\n    /**\n     * Sends an email notification for a termination of instance to a global\n     * email address.\n     * @param group the instance group\n     * @param instance the instance id\n     * @param chaosType the chosen chaos strategy\n     */\n    public abstract void sendTerminationGlobalNotification(InstanceGroup group, String instance, ChaosType chaosType);\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/ChaosInstance.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport org.jclouds.domain.LoginCredentials;\nimport org.jclouds.ssh.SshClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.CloudClient;\n\n/**\n * Wrapper around an instance on which we are going to cause chaos.\n */\npublic class ChaosInstance {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ChaosInstance.class);\n\n    private final CloudClient cloudClient;\n    private final String instanceId;\n    private final SshConfig sshConfig;\n\n    /**\n     * Constructor.\n     *\n     * @param cloudClient\n     *            client for cloud access\n     * @param instanceId\n     *            id of instance on cloud\n     * @param sshConfig\n     *            SSH configuration to access instance\n     */\n    public ChaosInstance(CloudClient cloudClient, String instanceId, SshConfig sshConfig) {\n        this.cloudClient = cloudClient;\n        this.instanceId = instanceId;\n        this.sshConfig = sshConfig;\n    }\n\n    /**\n     * Gets the {@link SshConfig} used to SSH to the instance.\n     *\n     * @return the {@link SshConfig}\n     */\n    public SshConfig getSshConfig() {\n        return sshConfig;\n    }\n\n    /**\n     * Gets the {@link CloudClient} used to access the cloud.\n     *\n     * @return the {@link CloudClient}\n     */\n    public CloudClient getCloudClient() {\n        return cloudClient;\n    }\n\n    /**\n     * Returns the instance id to identify the instance to the cloud client.\n     *\n     * @return instance id\n     */\n    public String getInstanceId() {\n        return instanceId;\n    }\n\n    /**\n     * Memoize canConnectSsh function.\n     */\n    private Boolean canConnectSsh = null;\n\n    /**\n     * Check if the SSH credentials are working.\n     *\n     * This is cached for the duration of this object.\n     *\n     * @return true iff ssh is configured and able to log on to instance.\n     */\n    public boolean canConnectSsh(ChaosInstance instance) {\n        if (!sshConfig.isEnabled()) {\n            return false;\n        }\n\n        if (canConnectSsh == null) {\n            try {\n                // It would be nicer to keep this connection open, but then we'd have to be closed.\n                SshClient client = connectSsh();\n                client.disconnect();\n                canConnectSsh = true;\n            } catch (Exception e) {\n                LOGGER.warn(\"Error making SSH connection to instance\", e);\n                canConnectSsh = false;\n            }\n        }\n        return canConnectSsh;\n    }\n\n    /**\n     * Connect to the instance over SSH.\n     *\n     * @return {@link SshClient} for connection\n     */\n    public SshClient connectSsh() {\n        if (!sshConfig.isEnabled()) {\n            throw new IllegalStateException();\n        }\n\n        LoginCredentials credentials = sshConfig.getCredentials();\n        SshClient ssh = cloudClient.connectSsh(instanceId, credentials);\n\n        return ssh;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/ChaosInstanceSelector.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\n\nimport java.util.Collection;\n\n/**\n * The Interface ChaosInstanceSelector.\n */\npublic interface ChaosInstanceSelector {\n\n    /**\n     * Select. Pick random instances out of the group with provided probability. Chaos will draw a random number and if\n     * that random number is lower than probability then it will proceed to select an instance (at random) out of the\n     * group. If the random number is higher than the provided probability then no instance will be selected and\n     * <b>null</b> will be returned.\n     *\n     * When the probability value is bigger than 1, say N + 0.x, it will first applies the algorithm described above\n     * with the probability value as 0.x to select possibly one instance, then it will randomly pick N instances.\n     *\n     * The probability is the run probability. If Chaos is running hourly between 9am and 3pm with an overall configured\n     * probability of \"1.0\" then the probability provided to this routine would be 1.0/6 (6 hours in 9am-3pm). So the\n     * typical probability here would be .1666. For Chaos to select an instance it will pick a random number between 0\n     * and 1. If that random number is less than the .1666 it will proceed to select an instance and return it,\n     * otherwise it will return null. Over 6 runs it is likely that the random number be less than .1666, but it is not\n     * certain.\n     *\n     * To make Chaos select an instance with 100% certainty it would have to be configured to run only once a day and\n     * the instance group would have to be configured for \"1.0\" daily probability.\n     *\n     * @param group\n     *            the group\n     * @param probability\n     *            the probability per run that an instance should be terminated.\n     * @return the instance\n     */\n    Collection<String> select(InstanceGroup group, double probability);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/ChaosMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport java.util.Date;\nimport java.util.List;\n\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.FeatureNotEnabledException;\nimport com.netflix.simianarmy.InstanceGroupNotFoundException;\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.MonkeyType;\n\n/**\n * The Class ChaosMonkey.\n */\npublic abstract class ChaosMonkey extends Monkey {\n\n    /**\n     * The Interface Context.\n     */\n    public interface Context extends Monkey.Context {\n\n        /**\n         * Configuration.\n         *\n         * @return the monkey configuration\n         */\n        MonkeyConfiguration configuration();\n\n        /**\n         * Chaos crawler.\n         *\n         * @return the chaos crawler\n         */\n        ChaosCrawler chaosCrawler();\n\n        /**\n         * Chaos instance selector.\n         *\n         * @return the chaos instance selector\n         */\n        ChaosInstanceSelector chaosInstanceSelector();\n\n        /**\n         * Chaos email notifier.\n         *\n         * @return the chaos email notifier\n         */\n        ChaosEmailNotifier chaosEmailNotifier();\n    }\n\n    /** The context. */\n    private final Context ctx;\n\n    /**\n     * Instantiates a new chaos monkey.\n     *\n     * @param ctx\n     *            the context.\n     */\n    public ChaosMonkey(Context ctx) {\n        super(ctx);\n        this.ctx = ctx;\n    }\n\n    /**\n     * The monkey Type.\n     */\n    public enum Type implements MonkeyType {\n\n        /** chaos monkey. */\n        CHAOS\n    }\n\n    /**\n     * The event types that this monkey causes.\n     */\n    public enum EventTypes implements EventType {\n\n        /** The chaos termination. */\n        CHAOS_TERMINATION, CHAOS_TERMINATION_SKIPPED\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public final Type type() {\n        return Type.CHAOS;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Context context() {\n        return ctx;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public abstract void doMonkeyBusiness();\n\n    /**\n     * Gets the count of terminations since a specific time. Chaos should probably not continue to beat up an instance\n     * group if the count exceeds a threshold.\n     *\n     * @param group\n     *            the group\n     * @return true, if successful\n     */\n    public abstract int getPreviousTerminationCount(ChaosCrawler.InstanceGroup group, Date after);\n\n    /**\n     * Record termination. This is used to notify system owners of terminations and to record terminations so that Chaos\n     * does not continue to thrash the instance groups on later runs.\n     *\n     * @param group\n     *            the group\n     * @param instance\n     *            the instance\n     * @return the termination event\n     */\n    public abstract Event recordTermination(ChaosCrawler.InstanceGroup group, String instance, ChaosType chaosType);\n\n    /**\n     * Terminates one instance right away from an instance group when there are available instances.\n     * @param type\n     *            the type of the instance group\n     * @param name\n     *            the name of the instance group\n     * @return the termination event\n     * @throws FeatureNotEnabledException\n     * @throws InstanceGroupNotFoundException\n     */\n    public abstract Event terminateNow(String type, String name, ChaosType chaosType)\n            throws FeatureNotEnabledException, InstanceGroupNotFoundException;\n\n    /**\n     * Sends notification for the termination to the instance owners.\n     *\n     * @param group\n     *            the group\n     * @param instance\n     *            the instance\n     * @param chaosType\n     *            the chaos monkey strategy that was chosen\n     */\n    public abstract void sendTerminationNotification(ChaosCrawler.InstanceGroup group, String instance,\n            ChaosType chaosType);\n\n    /**\n     * Gets a list of all enabled chaos types for this ChaosMonkey.\n     */\n    public abstract List<ChaosType> getChaosTypes();\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/ChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport java.util.List;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.CloudClient;\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * A strategy pattern for different types of chaos the chaos monkey can cause.\n */\npublic abstract class ChaosType {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ChaosType.class);\n\n    /**\n     * Configuration for this chaos type.\n     */\n    private final MonkeyConfiguration config;\n\n    /**\n     * The unique key for the ChaosType.\n     */\n    private final String key;\n\n    /**\n     * Is this strategy enabled?\n     */\n    private final boolean enabled;\n\n    /**\n     * Protected constructor (abstract class).\n     *\n     * @param config\n     *            Configuration to use\n     * @param key\n     *            Unique key for the ChaosType strategy\n     */\n    protected ChaosType(MonkeyConfiguration config, String key) {\n        this.config = config;\n        this.key = key;\n        this.enabled = config.getBoolOrElse(getConfigurationPrefix() + \"enabled\", getEnabledDefault());\n\n        LOGGER.info(\"ChaosType: {}: enabled={}\", key, enabled);\n    }\n\n    /**\n     * If not specified, controls whether we default to enabled.\n     *\n     * Most ChaosTypes should be disabled by default, not least for legacy compatibility, but we want at least one\n     * strategy to be available.\n     */\n    protected boolean getEnabledDefault() {\n        return false;\n    }\n\n    /**\n     * Returns the configuration key prefix to use for this strategy.\n     */\n    protected String getConfigurationPrefix() {\n        return \"simianarmy.chaos.\" + key.toLowerCase() + \".\";\n    }\n\n    /**\n     * Returns the unique key for the ChaosType.\n     */\n    public String getKey() {\n        return key;\n    }\n\n    /**\n     * Checks if this chaos type can be applied to the given instance.\n     *\n     * For example, if the strategy was to detach all the EBS volumes, that only makes sense if there are EBS volumes to\n     * detach.\n     */\n    public boolean canApply(ChaosInstance instance) {\n        return isEnabled();\n    }\n\n    /**\n     * Returns whether we are enabled.\n     */\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    /**\n     * Applies this chaos type to the specified instance.\n     */\n    public abstract void apply(ChaosInstance instance);\n\n    /**\n     * Returns the ChaosType with the matching key.\n     */\n    public static ChaosType parse(List<ChaosType> all, String chaosTypeName) {\n        for (ChaosType chaosType : all) {\n            if (chaosType.getKey().equalsIgnoreCase(chaosTypeName)) {\n                return chaosType;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown chaos type value: \"\n                + chaosTypeName);\n    }\n\n    /**\n     * Returns whether chaos types that cost money are allowed.\n     */\n    protected boolean isBurnMoneyEnabled() {\n        return config.getBoolOrElse(\"simianarmy.chaos.burnmoney\", false);\n    }\n\n    /**\n     * Checks whether the root volume of the specified instance is on EBS.\n     *\n     * @param instance id of instance\n     * @return true iff root is on EBS\n     */\n    protected boolean isRootVolumeEbs(ChaosInstance instance) {\n        CloudClient cloudClient = instance.getCloudClient();\n        String instanceId = instance.getInstanceId();\n\n        List<String> withRoot = cloudClient.listAttachedVolumes(instanceId, true);\n        List<String> withoutRoot = cloudClient.listAttachedVolumes(instanceId, false);\n\n        return (withRoot.size() != withoutRoot.size());\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/DetachVolumesChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport java.util.List;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.CloudClient;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.basic.chaos.BasicChaosMonkey;\n\n/**\n * We force-detach all the EBS volumes.\n *\n * This is supposed to simulate a catastrophic failure of EBS, however the instance will (possibly) still keep running;\n * e.g. it should continue to respond to pings.\n */\npublic class DetachVolumesChaosType extends ChaosType {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(BasicChaosMonkey.class);\n\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public DetachVolumesChaosType(MonkeyConfiguration config) {\n        super(config, \"DetachVolumes\");\n    }\n\n    /**\n     * Strategy can be applied iff there are any EBS volumes attached.\n     */\n    @Override\n    public boolean canApply(ChaosInstance instance) {\n        CloudClient cloudClient = instance.getCloudClient();\n        String instanceId = instance.getInstanceId();\n\n        List<String> volumes = cloudClient.listAttachedVolumes(instanceId, false);\n        if (volumes.isEmpty()) {\n            LOGGER.debug(\"Can't apply strategy: no non-root EBS volumes\");\n            return false;\n        }\n\n        return super.canApply(instance);\n    }\n\n    /**\n     * Force-detaches all attached EBS volumes from the instance.\n     */\n    @Override\n    public void apply(ChaosInstance instance) {\n        CloudClient cloudClient = instance.getCloudClient();\n        String instanceId = instance.getInstanceId();\n\n        // IDEA: We could have a strategy where we detach some of the volumes...\n        boolean force = true;\n        for (String volumeId : cloudClient.listAttachedVolumes(instanceId, false)) {\n            cloudClient.detachVolume(instanceId, volumeId, force);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/FailDnsChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Blocks TCP and UDP port 53, so DNS resolution fails.\n */\npublic class FailDnsChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public FailDnsChaosType(MonkeyConfiguration config) {\n        super(config, \"FailDns\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/FailDynamoDbChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Adds entries to /etc/hosts so that DynamoDB API endpoints are unreachable.\n */\npublic class FailDynamoDbChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public FailDynamoDbChaosType(MonkeyConfiguration config) {\n        super(config, \"FailDynamoDb\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/FailEc2ChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Adds entries to /etc/hosts so that EC2 API endpoints are unreachable.\n */\npublic class FailEc2ChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public FailEc2ChaosType(MonkeyConfiguration config) {\n        super(config, \"FailEc2\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/FailS3ChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Adds entries to /etc/hosts so that S3 API endpoints are unreachable.\n */\npublic class FailS3ChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public FailS3ChaosType(MonkeyConfiguration config) {\n        super(config, \"FailS3\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/FillDiskChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Creates a huge file on the root device so that the disk fills up.\n */\npublic class FillDiskChaosType extends ScriptChaosType {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(FillDiskChaosType.class);\n\n    /**\n     * Enhancement: As with BurnIoChaosType, it would be nice to randomize the volume.\n     *\n     * coryb suggested this, and proposed this script:\n     *\n     * nohup dd if=/dev/urandom of=/burn bs=1M count=$(df -ml /burn  | awk '/\\//{print $2}') iflag=fullblock &\n     */\n\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public FillDiskChaosType(MonkeyConfiguration config) {\n        super(config, \"FillDisk\");\n    }\n\n    @Override\n    public boolean canApply(ChaosInstance instance) {\n        if (!super.canApply(instance)) {\n            return false;\n        }\n\n        if (isRootVolumeEbs(instance) && !isBurnMoneyEnabled()) {\n            LOGGER.debug(\"Root volume is EBS so FillDisk would cost money; skipping\");\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/KillProcessesChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Kills processes on the node.\n *\n * This simulates the process crashing (for any reason).\n */\npublic class KillProcessesChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public KillProcessesChaosType(MonkeyConfiguration config) {\n        super(config, \"KillProcesses\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/NetworkCorruptionChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Introduces network packet corruption using traffic-shaping.\n */\npublic class NetworkCorruptionChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public NetworkCorruptionChaosType(MonkeyConfiguration config) {\n        super(config, \"NetworkCorruption\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/NetworkLatencyChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Introduces network latency using traffic-shaping.\n */\npublic class NetworkLatencyChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public NetworkLatencyChaosType(MonkeyConfiguration config) {\n        super(config, \"NetworkLatency\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/NetworkLossChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Introduces network packet loss using traffic-shaping.\n */\npublic class NetworkLossChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public NetworkLossChaosType(MonkeyConfiguration config) {\n        super(config, \"NetworkLoss\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/NullRouteChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Null routes the network, taking a node going offline.\n *\n * Currently we offline 10.x.x.x (the AWS private network range).\n *\n * I think the machine will still be publicly accessible, but won't be able to communicate with any other nodes on\n * the EC2 network.\n */\npublic class NullRouteChaosType extends ScriptChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public NullRouteChaosType(MonkeyConfiguration config) {\n        super(config, \"NullRoute\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/ScriptChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport java.io.IOException;\nimport java.net.URL;\nimport org.jclouds.compute.domain.ExecResponse;\nimport org.jclouds.ssh.SshClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.google.common.base.Charsets;\nimport com.google.common.io.Resources;\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Base class for chaos types that run a script over JClouds/SSH on the node.\n */\npublic abstract class ScriptChaosType extends ChaosType {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ScriptChaosType.class);\n\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     * @param key\n     *            Key for the chaos money\n     */\n    public ScriptChaosType(MonkeyConfiguration config, String key) {\n        super(config, key);\n    }\n\n    /**\n     * We can apply the strategy iff we can SSH to the instance.\n     */\n    @Override\n    public boolean canApply(ChaosInstance instance) {\n        if (!instance.getSshConfig().isEnabled()) {\n            LOGGER.info(\"Strategy disabled because SSH credentials not set\");\n            return false;\n        }\n\n        if (!instance.canConnectSsh(instance)) {\n            LOGGER.warn(\"Strategy disabled because SSH credentials failed\");\n            return false;\n        }\n\n        return super.canApply(instance);\n    }\n\n    /**\n     * Runs the script.\n     */\n    @Override\n    public void apply(ChaosInstance instance) {\n        LOGGER.info(\"Running script for {} on instance {}\", getKey(), instance.getInstanceId());\n\n        SshClient ssh = instance.connectSsh();\n\n        String filename = getKey().toLowerCase() + \".sh\";\n        URL url = Resources.getResource(ScriptChaosType.class, \"/scripts/\" + filename);\n        String script;\n        try {\n            script = Resources.toString(url, Charsets.UTF_8);\n        } catch (IOException e) {\n            throw new IllegalStateException(\"Error reading script resource\", e);\n        }\n\n        ssh.put(\"/tmp/\" + filename, script);\n        ExecResponse response = ssh.exec(\"/bin/bash /tmp/\" + filename);\n        if (response.getExitStatus() != 0) {\n            LOGGER.warn(\"Got non-zero output from running script: {}\", response);\n        }\n        ssh.disconnect();\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/ShutdownInstanceChaosType.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport com.netflix.simianarmy.CloudClient;\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Shuts down the instance using the cloud instance-termination API.\n *\n * This is the classic chaos-monkey strategy.\n */\npublic class ShutdownInstanceChaosType extends ChaosType {\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public ShutdownInstanceChaosType(MonkeyConfiguration config) {\n        super(config, \"ShutdownInstance\");\n    }\n\n    /**\n     * Shuts down the instance.\n     */\n    @Override\n    public void apply(ChaosInstance instance) {\n        CloudClient cloudClient = instance.getCloudClient();\n        String instanceId = instance.getInstanceId();\n\n        cloudClient.terminateInstance(instanceId);\n    }\n\n    /**\n     * We want to default to enabled.\n     */\n    @Override\n    protected boolean getEnabledDefault() {\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/chaos/SshConfig.java",
    "content": "/*\n *\n *  Copyright 2013 Justin Santa Barbara.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.chaos;\n\nimport java.io.File;\nimport java.io.IOException;\nimport org.jclouds.domain.LoginCredentials;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.google.common.base.Charsets;\nimport com.google.common.base.Strings;\nimport com.google.common.io.Files;\nimport com.netflix.simianarmy.MonkeyConfiguration;\n\n/**\n * Holds SSH connection info, used for script-based chaos types.\n */\npublic class SshConfig {\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(SshConfig.class);\n\n    /**\n     * The SSH credentials to log on to an instance.\n     */\n    private final LoginCredentials sshCredentials;\n\n    /**\n     * Constructor.\n     *\n     * @param config\n     *            Configuration to use\n     */\n    public SshConfig(MonkeyConfiguration config) {\n        String sshUser = config.getStrOrElse(\"simianarmy.chaos.ssh.user\", \"root\");\n        String privateKey = null;\n\n        String sshKeyPath = config.getStrOrElse(\"simianarmy.chaos.ssh.key\", null);\n        if (sshKeyPath != null) {\n            sshKeyPath = sshKeyPath.trim();\n            if (sshKeyPath.startsWith(\"~/\")) {\n                String home = System.getProperty(\"user.home\");\n                if (!Strings.isNullOrEmpty(home)) {\n                    if (!home.endsWith(\"/\")) {\n                        home += \"/\";\n                    }\n                    sshKeyPath = home + sshKeyPath.substring(2);\n                }\n            }\n\n            LOGGER.debug(\"Reading SSH key from {}\", sshKeyPath);\n\n            try {\n                privateKey = Files.toString(new File(sshKeyPath), Charsets.UTF_8);\n            } catch (IOException e) {\n                throw new IllegalStateException(\"Unable to read the specified SSH key: \" + sshKeyPath, e);\n            }\n        }\n\n        if (privateKey == null) {\n        \t   this.sshCredentials = LoginCredentials.builder().user(sshUser).build();\n        } else {\n            this.sshCredentials = LoginCredentials.builder().user(sshUser).privateKey(privateKey).build();\n        }\n    }\n\n    /**\n     * Get the configured SSH credentials.\n     *\n     * @return configured SSH credentials\n     */\n    public LoginCredentials getCredentials() {\n        return sshCredentials;\n    }\n\n    /**\n     * Check if ssh is configured.\n     *\n     * @return true if credentials are configured\n     */\n    public boolean isEnabled() {\n        return sshCredentials != null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/MonkeyRestClient.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.client;\n\nimport org.apache.commons.lang.Validate;\nimport org.apache.http.HttpResponse;\nimport org.apache.http.client.HttpClient;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy;\nimport org.apache.http.impl.client.HttpClientBuilder;\nimport org.codehaus.jackson.JsonNode;\nimport org.codehaus.jackson.map.ObjectMapper;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Scanner;\n\n/**\n * A REST client used by monkeys.\n */\npublic abstract class MonkeyRestClient {\n    private static final Logger LOGGER = LoggerFactory.getLogger(MonkeyRestClient.class);\n\n    private final HttpClient httpClient;\n\n    /**\n     * Constructor.\n     * @param timeout the timeout in milliseconds\n     * @param maxRetries the max number of retries\n     * @param retryInterval the interval in milliseconds between retries\n     */\n    public MonkeyRestClient(int timeout, int maxRetries, int retryInterval) {\n        Validate.isTrue(timeout >= 0);\n        Validate.isTrue(maxRetries >= 0);\n        Validate.isTrue(retryInterval > 0);\n\n        RequestConfig config = RequestConfig.custom()\n            .setConnectTimeout(timeout)\n            .build();\n        httpClient = HttpClientBuilder.create()\n            .setDefaultRequestConfig(config)\n            .setServiceUnavailableRetryStrategy(new DefaultServiceUnavailableRetryStrategy(maxRetries, retryInterval))\n            .build();\n    }\n\n    /**\n     * Gets the response in JSON from a url.\n     * @param url the url\n     * @return the JSON node for the response\n     */\n    // CHECKSTYLE IGNORE MagicNumberCheck\n    public JsonNode getJsonNodeFromUrl(String url) throws IOException {\n        LOGGER.info(String.format(\"Getting Json response from url: %s\", url));\n        HttpGet request = new HttpGet(url);\n        request.setHeader(\"Accept\", \"application/json\");\n        HttpResponse response = httpClient.execute(request);\n\n        InputStream is = response.getEntity().getContent();\n        String jsonContent;\n        if (is != null) {\n            Scanner s = new Scanner(is, \"UTF-8\").useDelimiter(\"\\\\A\");\n            jsonContent = s.hasNext() ? s.next() : \"\";\n            is.close();\n        } else {\n            return null;\n        }\n\n        int code = response.getStatusLine().getStatusCode();\n        if (code == 404) {\n            return null;\n        } else if (code >= 300 || code < 200) {\n            throw new DataReadException(code, url, jsonContent);\n        }\n\n        JsonNode result;\n        try {\n            ObjectMapper mapper = new ObjectMapper();\n            result = mapper.readTree(jsonContent);\n        } catch (Exception e) {\n            throw new RuntimeException(String.format(\"Error trying to parse json response from url %s, got: %s\",\n                    url, jsonContent), e);\n        }\n        return result;\n    }\n\n    /**\n     * Gets the base url of the service for a specific region.\n     * @param region the region\n     * @return the base url in the region\n     */\n    public abstract String getBaseUrl(String region);\n\n    public static class DataReadException extends RuntimeException {\n        public DataReadException(int code, String url, String jsonContent) {\n            super(String.format(\"Response code %d from url %s: %s\", code, url, jsonContent));\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/aws/AWSClient.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.client.aws;\n\nimport com.amazonaws.AmazonServiceException;\nimport com.amazonaws.ClientConfiguration;\nimport com.amazonaws.auth.AWSCredentials;\nimport com.amazonaws.auth.AWSCredentialsProvider;\nimport com.amazonaws.auth.AWSSessionCredentials;\nimport com.amazonaws.services.autoscaling.AmazonAutoScalingClient;\nimport com.amazonaws.services.autoscaling.model.*;\nimport com.amazonaws.services.ec2.AmazonEC2;\nimport com.amazonaws.services.ec2.AmazonEC2Client;\nimport com.amazonaws.services.ec2.model.*;\nimport com.amazonaws.services.ec2.model.Instance;\nimport com.amazonaws.services.ec2.model.Tag;\nimport com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingClient;\nimport com.amazonaws.services.elasticloadbalancing.model.*;\nimport com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersRequest;\nimport com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersResult;\nimport com.amazonaws.services.elasticloadbalancing.model.DescribeTagsRequest;\nimport com.amazonaws.services.elasticloadbalancing.model.DescribeTagsResult;\nimport com.amazonaws.services.elasticloadbalancing.model.TagDescription;\nimport com.amazonaws.services.route53.AmazonRoute53Client;\nimport com.amazonaws.services.route53.model.*;\nimport com.amazonaws.services.simpledb.AmazonSimpleDB;\nimport com.amazonaws.services.simpledb.AmazonSimpleDBClient;\nimport com.google.common.base.Objects;\nimport com.google.common.base.Strings;\nimport com.google.common.base.Suppliers;\nimport com.google.common.collect.ImmutableSet;\nimport com.google.common.collect.Iterables;\nimport com.google.common.collect.Sets;\nimport com.google.inject.Module;\nimport com.netflix.simianarmy.CloudClient;\nimport com.netflix.simianarmy.NotFoundException;\n\nimport org.apache.commons.lang.Validate;\nimport org.jclouds.ContextBuilder;\nimport org.jclouds.aws.domain.SessionCredentials;\nimport org.jclouds.compute.ComputeService;\nimport org.jclouds.compute.ComputeServiceContext;\nimport org.jclouds.compute.Utils;\nimport org.jclouds.compute.domain.ComputeMetadata;\nimport org.jclouds.compute.domain.NodeMetadata;\nimport org.jclouds.compute.domain.NodeMetadataBuilder;\nimport org.jclouds.domain.Credentials;\nimport org.jclouds.domain.LoginCredentials;\nimport org.jclouds.logging.slf4j.config.SLF4JLoggingModule;\nimport org.jclouds.ssh.SshClient;\nimport org.jclouds.ssh.jsch.config.JschSshClientModule;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.*;\n\n\n\n/**\n * The Class AWSClient. Simple Amazon EC2 and Amazon ASG client interface.\n */\npublic class AWSClient implements CloudClient {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(AWSClient.class);\n\n    /** The region. */\n    private final String region;\n\n    /** The plain name for AWS account */\n    private final String accountName;\n\n    /** Maximum retry count for Simple DB */\n    private static final int SIMPLE_DB_MAX_RETRY = 11;\n\n    private final AWSCredentialsProvider awsCredentialsProvider;\n\n    private final ClientConfiguration awsClientConfig;\n\n    private ComputeService jcloudsComputeService;\n    \n    \n\n    /**\n     * This constructor will let the AWS SDK obtain the credentials, which will\n     * choose such in the following order:\n     *\n     * <ul>\n     * <li>Environment Variables: {@code AWS_ACCESS_KEY_ID} and\n     * {@code AWS_SECRET_KEY}</li>\n     * <li>Java System Properties: {@code aws.accessKeyId} and\n     * {@code aws.secretKey}</li>\n     * <li>Instance Metadata Service, which provides the credentials associated\n     * with the IAM role for the EC2 instance</li>\n     * </ul>\n     *\n     * <p>\n     * If credentials are provided explicitly, use\n     * {@link com.netflix.simianarmy.basic.BasicSimianArmyContext#exportCredentials(String, String)}\n     * which will set them as System properties used by each AWS SDK call.\n     * </p>\n     *\n     * <p>\n     * <b>Note:</b> Avoid storing credentials received dynamically via the\n     * {@link com.amazonaws.auth.InstanceProfileCredentialsProvider} as these will be rotated and\n     * their renewal is handled by its\n     * {@link com.amazonaws.auth.InstanceProfileCredentialsProvider#getCredentials()} method.\n     * </p>\n     *\n     * @param region\n     *            the region\n     * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain\n     * @see com.amazonaws.auth.InstanceProfileCredentialsProvider\n     * @see com.netflix.simianarmy.basic.BasicSimianArmyContext#exportCredentials(String, String)\n     */\n    public AWSClient(String region) {\n        this.region = region;\n        this.accountName = \"Default\";\n        this.awsCredentialsProvider = null;\n        this.awsClientConfig = null;\n    }\n\n    /**\n     * The constructor allows you to provide your own AWS credentials provider.\n     * @param region\n     *          the region\n     * @param awsCredentialsProvider\n     *          the AWS credentials provider\n     */\n    public AWSClient(String region, AWSCredentialsProvider awsCredentialsProvider) {\n        this.region = region;\n        this.accountName = \"Default\";\n        this.awsCredentialsProvider = awsCredentialsProvider;\n        this.awsClientConfig = null;\n    }\n\n    /**\n     * The constructor allows you to provide your own AWS client configuration.\n     * @param region\n     *          the region\n     * @param awsClientConfig\n     *          the AWS client configuration\n     */\n    public AWSClient(String region, ClientConfiguration awsClientConfig) {\n        this.region = region;\n        this.accountName = \"Default\";\n        this.awsCredentialsProvider = null;\n        this.awsClientConfig = awsClientConfig;\n    }\n\n    /**\n     * The constructor allows you to provide your own AWS credentials provider and client config.\n     * @param region\n     *          the region\n     * @param awsCredentialsProvider\n     *          the AWS credentials provider\n     * @param awsClientConfig\n     *          the AWS client configuration\n     */\n    public AWSClient(String region, AWSCredentialsProvider awsCredentialsProvider, ClientConfiguration awsClientConfig) {\n        this.region = region;\n        this.accountName = \"Default\";\n        this.awsCredentialsProvider = awsCredentialsProvider;\n        this.awsClientConfig = awsClientConfig;\n    }\n\n    /**\n     * The Region.\n     *\n     * @return the region the client is configured to communicate with\n     */\n    public String region() {\n        return region;\n    }\n\n    /**\n     * The accountName.\n     *\n     * @return the plain name for the aws account easier to identify which account\n     * monkey is running in\n     */\n    public String accountName() {\n        return accountName;\n    }\n\n    /**\n     * Amazon EC2 client. Abstracted to aid testing.\n     *\n     * @return the Amazon EC2 client\n     */\n    protected AmazonEC2 ec2Client() {\n        AmazonEC2 client;\n        if (awsClientConfig == null) {\n            if (awsCredentialsProvider == null) {\n                client = new AmazonEC2Client();\n            } else {\n                client = new AmazonEC2Client(awsCredentialsProvider);\n            }\n        } else {\n            if (awsCredentialsProvider == null) {\n                client = new AmazonEC2Client(awsClientConfig);\n            } else {\n                client = new AmazonEC2Client(awsCredentialsProvider, awsClientConfig);\n            }\n        }\n        client.setEndpoint(\"ec2.\" + region + \".amazonaws.com\");\n        return client;\n    }\n\n    /**\n     * Amazon ASG client. Abstracted to aid testing.\n     *\n     * @return the Amazon Auto Scaling client\n     */\n    protected AmazonAutoScalingClient asgClient() {\n        AmazonAutoScalingClient client;\n        if (awsClientConfig == null) {\n            if (awsCredentialsProvider == null) {\n                client = new AmazonAutoScalingClient();\n            } else {\n                client = new AmazonAutoScalingClient(awsCredentialsProvider);\n            }\n        } else {\n            if (awsCredentialsProvider == null) {\n                client = new AmazonAutoScalingClient(awsClientConfig);\n            } else {\n                client = new AmazonAutoScalingClient(awsCredentialsProvider, awsClientConfig);\n            }\n        }\n        client.setEndpoint(\"autoscaling.\" + region + \".amazonaws.com\");\n        return client;\n    }\n\n    /**\n     * Amazon ELB client. Abstracted to aid testing.\n     *\n     * @return the Amazon ELB client\n     */\n    protected AmazonElasticLoadBalancingClient elbClient() {\n        AmazonElasticLoadBalancingClient client;\n        if (awsClientConfig == null) {\n            if (awsCredentialsProvider == null) {\n                client = new AmazonElasticLoadBalancingClient();\n            } else {\n                client = new AmazonElasticLoadBalancingClient(awsCredentialsProvider);\n            }\n        } else {\n            if (awsCredentialsProvider == null) {\n                client = new AmazonElasticLoadBalancingClient(awsClientConfig);\n            } else {\n                client = new AmazonElasticLoadBalancingClient(awsCredentialsProvider, awsClientConfig);\n            }\n        }\n        client.setEndpoint(\"elasticloadbalancing.\" + region + \".amazonaws.com\");\n        return client;\n    }\n\n    /**\n     * Amazon Route53 client. Abstracted to aid testing.\n     *\n     * @return the Amazon Route53 client\n     */\n    protected AmazonRoute53Client route53Client() {\n        AmazonRoute53Client client;\n        if (awsClientConfig == null) {\n            if (awsCredentialsProvider == null) {\n                client = new AmazonRoute53Client();\n            } else {\n                client = new AmazonRoute53Client(awsCredentialsProvider);\n            }\n        } else {\n            if (awsCredentialsProvider == null) {\n                client = new AmazonRoute53Client(awsClientConfig);\n            } else {\n                client = new AmazonRoute53Client(awsCredentialsProvider, awsClientConfig);\n            }\n        }\n        client.setEndpoint(\"route53.amazonaws.com\");\n        return client;\n    }\n\n    /**\n     * Amazon SimpleDB client.\n     *\n     * @return the Amazon SimpleDB client\n     */\n    public AmazonSimpleDB sdbClient() {\n        AmazonSimpleDB client;\n        ClientConfiguration cc = awsClientConfig;\n        \n        if (cc == null) { \n          cc = new ClientConfiguration();\n          cc.setMaxErrorRetry(SIMPLE_DB_MAX_RETRY);\n        }\n        \n        if (awsCredentialsProvider == null) {\n            client = new AmazonSimpleDBClient(cc);\n        } else {\n            client = new AmazonSimpleDBClient(awsCredentialsProvider, cc);\n        }\n        \n        // us-east-1 has special naming\n        // http://docs.amazonwebservices.com/general/latest/gr/rande.html#sdb_region\n        if (region == null || region.equals(\"us-east-1\")) {\n            client.setEndpoint(\"sdb.amazonaws.com\");\n        } else {\n            client.setEndpoint(\"sdb.\" + region + \".amazonaws.com\");\n        }\n        return client;\n    }\n    \n    /**\n     * Describe auto scaling groups.\n     *\n     * @return the list\n     */\n    public List<AutoScalingGroup> describeAutoScalingGroups() {\n        return describeAutoScalingGroups((String[]) null);\n    }\n\n    /**\n     * Describe a set of specific auto scaling groups.\n     *\n     * @param names the ASG names\n     * @return the auto scaling groups\n     */\n    public List<AutoScalingGroup> describeAutoScalingGroups(String... names) {\n        if (names == null || names.length == 0) {\n            LOGGER.info(String.format(\"Getting all auto-scaling groups in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting auto-scaling groups for %d names in region %s.\", names.length, region));\n        }\n\n        List<AutoScalingGroup> asgs = new LinkedList<AutoScalingGroup>();\n\n        AmazonAutoScalingClient asgClient = asgClient();\n        DescribeAutoScalingGroupsRequest request = new DescribeAutoScalingGroupsRequest();\n        if (names != null) {\n            request.setAutoScalingGroupNames(Arrays.asList(names));\n        }\n        DescribeAutoScalingGroupsResult result = asgClient.describeAutoScalingGroups(request);\n\n        asgs.addAll(result.getAutoScalingGroups());\n        while (result.getNextToken() != null) {\n            request.setNextToken(result.getNextToken());\n            result = asgClient.describeAutoScalingGroups(request);\n            asgs.addAll(result.getAutoScalingGroups());\n        }\n\n        LOGGER.info(String.format(\"Got %d auto-scaling groups in region %s.\", asgs.size(), region));\n        return asgs;\n    }\n\n    /**\n     * Describe a set of specific ELBs.\n     *\n     * @param names the ELB names\n     * @return the ELBs\n     */\n    public List<LoadBalancerDescription> describeElasticLoadBalancers(String... names) {\n        if (names == null || names.length == 0) {\n            LOGGER.info(String.format(\"Getting all ELBs in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting ELBs for %d names in region %s.\", names.length, region));\n        }\n\n        AmazonElasticLoadBalancingClient elbClient = elbClient();\n        DescribeLoadBalancersRequest request = new DescribeLoadBalancersRequest().withLoadBalancerNames(names);\n        DescribeLoadBalancersResult result = elbClient.describeLoadBalancers(request);\n        List<LoadBalancerDescription> elbs = result.getLoadBalancerDescriptions();\n        LOGGER.info(String.format(\"Got %d ELBs in region %s.\", elbs.size(), region));\n        return elbs;\n    }\n\n    /**\n     * Describe a specific ELB.\n     *\n     * @param name the ELB names\n     * @return the ELBs\n     */\n    public LoadBalancerAttributes describeElasticLoadBalancerAttributes(String name) {\n        LOGGER.info(String.format(\"Getting attributes for ELB with name '%s' in region %s.\", name, region));\n        AmazonElasticLoadBalancingClient elbClient = elbClient();\n        DescribeLoadBalancerAttributesRequest request = new DescribeLoadBalancerAttributesRequest().withLoadBalancerName(name);\n        DescribeLoadBalancerAttributesResult result = elbClient.describeLoadBalancerAttributes(request);\n        LoadBalancerAttributes attrs = result.getLoadBalancerAttributes();\n        LOGGER.info(String.format(\"Got attributes for ELB with name '%s' in region %s.\", name, region));\n        return attrs;\n    }\n\n    /**\n     * Retreive the tags for a specific ELB.\n     *\n     * @param name the ELB names\n     * @return the ELBs\n     */\n    public List<TagDescription> describeElasticLoadBalancerTags(String name) {\n        LOGGER.info(String.format(\"Getting tags for ELB with name '%s' in region %s.\", name, region));\n        AmazonElasticLoadBalancingClient elbClient = elbClient();\n        DescribeTagsRequest request = new DescribeTagsRequest().withLoadBalancerNames(name);\n        DescribeTagsResult result = elbClient.describeTags(request);\n        LOGGER.info(String.format(\"Got tags for ELB with name '%s' in region %s.\", name, region));\n        return result.getTagDescriptions();\n    }\n\n    /**\n     * Describe a set of specific auto-scaling instances.\n     *\n     * @param instanceIds the instance ids\n     * @return the instances\n     */\n    public List<AutoScalingInstanceDetails> describeAutoScalingInstances(String... instanceIds) {\n        if (instanceIds == null || instanceIds.length == 0) {\n            LOGGER.info(String.format(\"Getting all auto-scaling instances in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting auto-scaling instances for %d ids in region %s.\",\n                    instanceIds.length, region));\n        }\n\n        List<AutoScalingInstanceDetails> instances = new LinkedList<AutoScalingInstanceDetails>();\n\n        AmazonAutoScalingClient asgClient = asgClient();\n        DescribeAutoScalingInstancesRequest request = new DescribeAutoScalingInstancesRequest();\n        if (instanceIds != null) {\n            request.setInstanceIds(Arrays.asList(instanceIds));\n        }\n        DescribeAutoScalingInstancesResult result = asgClient.describeAutoScalingInstances(request);\n\n        instances.addAll(result.getAutoScalingInstances());\n        while (result.getNextToken() != null) {\n            request = request.withNextToken(result.getNextToken());\n            result = asgClient.describeAutoScalingInstances(request);\n            instances.addAll(result.getAutoScalingInstances());\n        }\n\n        LOGGER.info(String.format(\"Got %d auto-scaling instances.\", instances.size()));\n        return instances;\n    }\n\n    /**\n     * Describe a set of specific instances.\n     *\n     * @param instanceIds the instance ids\n     * @return the instances\n     */\n    public List<Instance> describeInstances(String... instanceIds) {\n        if (instanceIds == null || instanceIds.length == 0) {\n            LOGGER.info(String.format(\"Getting all EC2 instances in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting EC2 instances for %d ids in region %s.\", instanceIds.length, region));\n        }\n\n        List<Instance> instances = new LinkedList<Instance>();\n\n        AmazonEC2 ec2Client = ec2Client();\n        DescribeInstancesRequest request = new DescribeInstancesRequest();\n        if (instanceIds != null) {\n            request.withInstanceIds(Arrays.asList(instanceIds));\n        }\n        DescribeInstancesResult result = ec2Client.describeInstances(request);\n        for (Reservation reservation : result.getReservations()) {\n            instances.addAll(reservation.getInstances());\n        }\n\n        LOGGER.info(String.format(\"Got %d EC2 instances in region %s.\", instances.size(), region));\n        return instances;\n    }\n\n    /**\n     * Describe a set of specific launch configurations.\n     *\n     * @param names the launch configuration names\n     * @return the launch configurations\n     */\n    public List<LaunchConfiguration> describeLaunchConfigurations(String... names) {\n        if (names == null || names.length == 0) {\n            LOGGER.info(String.format(\"Getting all launch configurations in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting launch configurations for %d names in region %s.\",\n                    names.length, region));\n        }\n\n        List<LaunchConfiguration> lcs = new LinkedList<LaunchConfiguration>();\n\n        AmazonAutoScalingClient asgClient = asgClient();\n        DescribeLaunchConfigurationsRequest request = new DescribeLaunchConfigurationsRequest()\n        .withLaunchConfigurationNames(names);\n        DescribeLaunchConfigurationsResult result = asgClient.describeLaunchConfigurations(request);\n\n        lcs.addAll(result.getLaunchConfigurations());\n        while (result.getNextToken() != null) {\n            request.setNextToken(result.getNextToken());\n            result = asgClient.describeLaunchConfigurations(request);\n            lcs.addAll(result.getLaunchConfigurations());\n        }\n\n        LOGGER.info(String.format(\"Got %d launch configurations in region %s.\", lcs.size(), region));\n        return lcs;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void deleteAutoScalingGroup(String asgName) {\n        Validate.notEmpty(asgName);\n        LOGGER.info(String.format(\"Deleting auto-scaling group with name %s in region %s.\", asgName, region));\n        AmazonAutoScalingClient asgClient = asgClient();\n        DeleteAutoScalingGroupRequest request = new DeleteAutoScalingGroupRequest()\n        .withAutoScalingGroupName(asgName).withForceDelete(true);\n        try {\n            asgClient.deleteAutoScalingGroup(request);\n            LOGGER.info(String.format(\"Deleted auto-scaling group with name %s in region %s.\", asgName, region));\n        }catch(Exception e) {\n            LOGGER.error(\"Got an exception deleting ASG \" + asgName, e);\n        }\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void deleteLaunchConfiguration(String launchConfigName) {\n        Validate.notEmpty(launchConfigName);\n        LOGGER.info(String.format(\"Deleting launch configuration with name %s in region %s.\",\n                launchConfigName, region));\n        AmazonAutoScalingClient asgClient = asgClient();\n        DeleteLaunchConfigurationRequest request = new DeleteLaunchConfigurationRequest()\n                .withLaunchConfigurationName(launchConfigName);\n        asgClient.deleteLaunchConfiguration(request);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void deleteImage(String imageId) {\n        Validate.notEmpty(imageId);\n        LOGGER.info(String.format(\"Deleting image %s in region %s.\",\n                imageId, region));\n        AmazonEC2 ec2Client = ec2Client();\n        DeregisterImageRequest request = new DeregisterImageRequest(imageId);\n        ec2Client.deregisterImage(request);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void deleteVolume(String volumeId) {\n        Validate.notEmpty(volumeId);\n        LOGGER.info(String.format(\"Deleting volume %s in region %s.\", volumeId, region));\n        AmazonEC2 ec2Client = ec2Client();\n        DeleteVolumeRequest request = new DeleteVolumeRequest().withVolumeId(volumeId);\n        ec2Client.deleteVolume(request);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void deleteSnapshot(String snapshotId) {\n        Validate.notEmpty(snapshotId);\n        LOGGER.info(String.format(\"Deleting snapshot %s in region %s.\", snapshotId, region));\n        AmazonEC2 ec2Client = ec2Client();\n        DeleteSnapshotRequest request = new DeleteSnapshotRequest().withSnapshotId(snapshotId);\n        ec2Client.deleteSnapshot(request);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void deleteElasticLoadBalancer(String elbId) {\n        Validate.notEmpty(elbId);\n        LOGGER.info(String.format(\"Deleting ELB %s in region %s.\", elbId, region));\n        AmazonElasticLoadBalancingClient elbClient = elbClient();\n        DeleteLoadBalancerRequest request = new DeleteLoadBalancerRequest(elbId);\n        elbClient.deleteLoadBalancer(request);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void deleteDNSRecord(String dnsName, String dnsType, String hostedZoneID) {\n        Validate.notEmpty(dnsName);\n        Validate.notEmpty(dnsType);\n\n        if(dnsType.equals(\"A\") || dnsType.equals(\"AAAA\") || dnsType.equals(\"CNAME\")) {\n            LOGGER.info(String.format(\"Deleting DNS Route 53 record %s\", dnsName));\n            AmazonRoute53Client route53Client = route53Client();\n\n            // AWS API requires us to query for the record first\n            ListResourceRecordSetsRequest listRequest = new ListResourceRecordSetsRequest(hostedZoneID);\n            listRequest.setMaxItems(\"1\");\n            listRequest.setStartRecordType(dnsType);\n            listRequest.setStartRecordName(dnsName);\n            ListResourceRecordSetsResult listResult = route53Client.listResourceRecordSets(listRequest);\n            if (listResult.getResourceRecordSets().size() < 1) {\n                throw new NotFoundException(\"Could not find Route53 record for \" + dnsName + \" (\" + dnsType + \") in zone \" + hostedZoneID);\n            } else {\n                ResourceRecordSet resourceRecord = listResult.getResourceRecordSets().get(0);\n                ArrayList<Change> changeList = new ArrayList<>();\n                Change recordChange = new Change(ChangeAction.DELETE, resourceRecord);\n                changeList.add(recordChange);\n                ChangeBatch recordChangeBatch = new ChangeBatch(changeList);\n\n                ChangeResourceRecordSetsRequest request = new ChangeResourceRecordSetsRequest(hostedZoneID, recordChangeBatch);\n                ChangeResourceRecordSetsResult result = route53Client.changeResourceRecordSets(request);\n            }\n        } else {\n            LOGGER.error(\"dnsType must be one of 'A', 'AAAA', or 'CNAME'\");\n        }\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public void terminateInstance(String instanceId) {\n        Validate.notEmpty(instanceId);\n        LOGGER.info(String.format(\"Terminating instance %s in region %s.\", instanceId, region));\n        try {\n            ec2Client().terminateInstances(new TerminateInstancesRequest(Arrays.asList(instanceId)));\n        } catch (AmazonServiceException e) {\n            if (e.getErrorCode().equals(\"InvalidInstanceID.NotFound\")) {\n                throw new NotFoundException(\"AWS instance \" + instanceId + \" not found\", e);\n            }\n            throw e;\n        }\n    }\n\n    /** {@inheritDoc} */\n    public void setInstanceSecurityGroups(String instanceId, List<String> groupIds) {\n        Validate.notEmpty(instanceId);\n        LOGGER.info(String.format(\"Removing all security groups from instance %s in region %s.\", instanceId, region));\n        try {\n            ModifyInstanceAttributeRequest request = new ModifyInstanceAttributeRequest();\n            request.setInstanceId(instanceId);\n            request.setGroups(groupIds);\n            ec2Client().modifyInstanceAttribute(request);\n        } catch (AmazonServiceException e) {\n            if (e.getErrorCode().equals(\"InvalidInstanceID.NotFound\")) {\n                throw new NotFoundException(\"AWS instance \" + instanceId + \" not found\", e);\n            }\n            throw e;\n        }\n    }\n\n    /**\n     * Describe a set of specific EBS volumes.\n     *\n     * @param volumeIds the volume ids\n     * @return the volumes\n     */\n    public List<Volume> describeVolumes(String... volumeIds) {\n        if (volumeIds == null || volumeIds.length == 0) {\n            LOGGER.info(String.format(\"Getting all EBS volumes in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting EBS volumes for %d ids in region %s.\", volumeIds.length, region));\n        }\n\n        AmazonEC2 ec2Client = ec2Client();\n        DescribeVolumesRequest request = new DescribeVolumesRequest();\n        if (volumeIds != null) {\n            request.setVolumeIds(Arrays.asList(volumeIds));\n        }\n        DescribeVolumesResult result = ec2Client.describeVolumes(request);\n        List<Volume> volumes = result.getVolumes();\n\n        LOGGER.info(String.format(\"Got %d EBS volumes in region %s.\", volumes.size(), region));\n        return volumes;\n    }\n\n    /**\n     * Describe a set of specific EBS snapshots.\n     *\n     * @param snapshotIds the snapshot ids\n     * @return the snapshots\n     */\n    public List<Snapshot> describeSnapshots(String... snapshotIds) {\n        if (snapshotIds == null || snapshotIds.length == 0) {\n            LOGGER.info(String.format(\"Getting all EBS snapshots in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting EBS snapshotIds for %d ids in region %s.\", snapshotIds.length, region));\n        }\n\n        AmazonEC2 ec2Client = ec2Client();\n        DescribeSnapshotsRequest request = new DescribeSnapshotsRequest();\n        // Set the owner id to self to avoid getting snapshots from other accounts.\n        request.withOwnerIds(Arrays.<String>asList(\"self\"));\n        if (snapshotIds != null) {\n            request.setSnapshotIds(Arrays.asList(snapshotIds));\n        }\n        DescribeSnapshotsResult result = ec2Client.describeSnapshots(request);\n        List<Snapshot> snapshots = result.getSnapshots();\n\n        LOGGER.info(String.format(\"Got %d EBS snapshots in region %s.\", snapshots.size(), region));\n        return snapshots;\n    }\n\n    @Override\n    public void createTagsForResources(Map<String, String> keyValueMap, String... resourceIds) {\n        Validate.notNull(keyValueMap);\n        Validate.notEmpty(keyValueMap);\n        Validate.notNull(resourceIds);\n        Validate.notEmpty(resourceIds);\n        AmazonEC2 ec2Client = ec2Client();\n        List<Tag> tags = new ArrayList<Tag>();\n        for (Map.Entry<String, String> entry : keyValueMap.entrySet()) {\n            tags.add(new Tag(entry.getKey(), entry.getValue()));\n        }\n        CreateTagsRequest req = new CreateTagsRequest(Arrays.asList(resourceIds), tags);\n        ec2Client.createTags(req);\n    }\n\n    /**\n     * Describe a set of specific images.\n     *\n     * @param imageIds the image ids\n     * @return the images\n     */\n    public List<Image> describeImages(String... imageIds) {\n        if (imageIds == null || imageIds.length == 0) {\n            LOGGER.info(String.format(\"Getting all AMIs in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting AMIs for %d ids in region %s.\", imageIds.length, region));\n        }\n\n        AmazonEC2 ec2Client = ec2Client();\n        DescribeImagesRequest request = new DescribeImagesRequest();\n        if (imageIds != null) {\n            request.setImageIds(Arrays.asList(imageIds));\n        }\n        DescribeImagesResult result = ec2Client.describeImages(request);\n        List<Image> images = result.getImages();\n\n        LOGGER.info(String.format(\"Got %d AMIs in region %s.\", images.size(), region));\n        return images;\n    }\n\n    @Override\n    public void detachVolume(String instanceId, String volumeId, boolean force) {\n        Validate.notEmpty(instanceId);\n        LOGGER.info(String.format(\"Detach volumes from instance %s in region %s.\", instanceId, region));\n        try {\n            DetachVolumeRequest detachVolumeRequest = new DetachVolumeRequest();\n            detachVolumeRequest.setForce(force);\n            detachVolumeRequest.setInstanceId(instanceId);\n            detachVolumeRequest.setVolumeId(volumeId);\n            ec2Client().detachVolume(detachVolumeRequest);\n        } catch (AmazonServiceException e) {\n            if (e.getErrorCode().equals(\"InvalidInstanceID.NotFound\")) {\n                throw new NotFoundException(\"AWS instance \" + instanceId + \" not found\", e);\n            }\n            throw e;\n        }\n    }\n\n    @Override\n    public List<String> listAttachedVolumes(String instanceId, boolean includeRoot) {\n        Validate.notEmpty(instanceId);\n        LOGGER.info(String.format(\"Listing volumes attached to instance %s in region %s.\", instanceId, region));\n        try {\n            List<String> volumeIds = new ArrayList<String>();\n            for (Instance instance : describeInstances(instanceId)) {\n                String rootDeviceName = instance.getRootDeviceName();\n\n                for (InstanceBlockDeviceMapping ibdm : instance.getBlockDeviceMappings()) {\n                    EbsInstanceBlockDevice ebs = ibdm.getEbs();\n                    if (ebs == null) {\n                        continue;\n                    }\n\n                    String volumeId = ebs.getVolumeId();\n                    if (Strings.isNullOrEmpty(volumeId)) {\n                        continue;\n                    }\n\n                    if (!includeRoot && rootDeviceName != null && rootDeviceName.equals(ibdm.getDeviceName())) {\n                        continue;\n                    }\n\n                    volumeIds.add(volumeId);\n                }\n            }\n            return volumeIds;\n        } catch (AmazonServiceException e) {\n            if (e.getErrorCode().equals(\"InvalidInstanceID.NotFound\")) {\n                throw new NotFoundException(\"AWS instance \" + instanceId + \" not found\", e);\n            }\n            throw e;\n        }\n    }\n\n    /**\n     * Describe a set of security groups.\n     *\n     * @param groupNames the names of the groups to find\n     * @return a list of matching groups\n     */\n    public List<SecurityGroup> describeSecurityGroups(String... groupNames) {\n        AmazonEC2 ec2Client = ec2Client();\n        DescribeSecurityGroupsRequest request = new DescribeSecurityGroupsRequest();\n\n        if (groupNames == null || groupNames.length == 0) {\n            LOGGER.info(String.format(\"Getting all EC2 security groups in region %s.\", region));\n        } else {\n            LOGGER.info(String.format(\"Getting EC2 security groups for %d names in region %s.\", groupNames.length,\n                    region));\n            request.withGroupNames(groupNames);\n        }\n\n        DescribeSecurityGroupsResult result;\n        try {\n            result = ec2Client.describeSecurityGroups(request);\n        } catch (AmazonServiceException e) {\n            if (e.getErrorCode().equals(\"InvalidGroup.NotFound\")) {\n                LOGGER.info(\"Got InvalidGroup.NotFound error for security groups; returning empty list\");\n                return Collections.emptyList();\n            }\n            throw e;\n        }\n\n        List<SecurityGroup> securityGroups = result.getSecurityGroups();\n        LOGGER.info(String.format(\"Got %d EC2 security groups in region %s.\", securityGroups.size(), region));\n        return securityGroups;\n    }\n\n    /** {@inheritDoc} */\n    public String createSecurityGroup(String instanceId, String name, String description) {\n        String vpcId = getVpcId(instanceId);\n\n        AmazonEC2 ec2Client = ec2Client();\n        CreateSecurityGroupRequest request = new CreateSecurityGroupRequest();\n        request.setGroupName(name);\n        request.setDescription(description);\n        request.setVpcId(vpcId);\n\n        LOGGER.info(String.format(\"Creating EC2 security group %s.\", name));\n\n        CreateSecurityGroupResult result = ec2Client.createSecurityGroup(request);\n        return result.getGroupId();\n    }\n\n    /**\n     * Convenience wrapper around describeInstances, for a single instance id.\n     *\n     * @param instanceId id of instance to find\n     * @return the instance info, or null if instance not found\n     */\n    public Instance describeInstance(String instanceId) {\n        Instance instance = null;\n        for (Instance i : describeInstances(instanceId)) {\n            if (instance != null) {\n                throw new IllegalStateException(\"Duplicate instance: \" + instanceId);\n            }\n            instance = i;\n        }\n        return instance;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public ComputeService getJcloudsComputeService() {\n        if (jcloudsComputeService == null) {\n            synchronized(this) {\n                if (jcloudsComputeService == null) {\n\t\t    AWSCredentials awsCredentials = awsCredentialsProvider.getCredentials();\n\t\t    String username = awsCredentials.getAWSAccessKeyId();\n\t\t    String password = awsCredentials.getAWSSecretKey();\n\n\t\t    Credentials credentials;\n\t\t    if (awsCredentials instanceof AWSSessionCredentials) {\n\t\t\tAWSSessionCredentials awsSessionCredentials = (AWSSessionCredentials) awsCredentials;\n\t\t\tcredentials = SessionCredentials.builder().accessKeyId(username).secretAccessKey(password)\n\t\t\t\t.sessionToken(awsSessionCredentials.getSessionToken()).build();\n\t\t    } else {\n\t\t\tcredentials = new Credentials(username, password);\n\t\t    }\n\n\t\t    ComputeServiceContext jcloudsContext = ContextBuilder.newBuilder(\"aws-ec2\")\n\t\t\t    .credentialsSupplier(Suppliers.ofInstance(credentials))\n\t\t\t    .modules(ImmutableSet.<Module>of(new SLF4JLoggingModule(), new JschSshClientModule()))\n\t\t\t    .buildView(ComputeServiceContext.class);\n\n                    this.jcloudsComputeService = jcloudsContext.getComputeService();\n                }\n            }\n        }\n\n        return jcloudsComputeService;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String getJcloudsId(String instanceId) {\n        return this.region + \"/\" + instanceId;\n    }\n\n    @Override\n    public SshClient connectSsh(String instanceId, LoginCredentials credentials) {\n        ComputeService computeService = getJcloudsComputeService();\n\n        String jcloudsId = getJcloudsId(instanceId);\n        NodeMetadata node = getJcloudsNode(computeService, jcloudsId);\n\n        node = NodeMetadataBuilder.fromNodeMetadata(node).credentials(credentials).build();\n\n        Utils utils = computeService.getContext().utils();\n        SshClient ssh = utils.sshForNode().apply(node);\n\n        ssh.connect();\n\n        return ssh;\n    }\n\n    private NodeMetadata getJcloudsNode(ComputeService computeService, String jcloudsId) {\n        // Work around a jclouds bug / documentation issue...\n        // TODO: Figure out what's broken, and eliminate this function\n\n        // This should work (?):\n        // Set<NodeMetadata> nodes = computeService.listNodesByIds(Collections.singletonList(jcloudsId));\n\n        Set<NodeMetadata> nodes = Sets.newHashSet();\n        for (ComputeMetadata n : computeService.listNodes()) {\n            if (jcloudsId.equals(n.getId())) {\n                nodes.add((NodeMetadata) n);\n            }\n        }\n\n        if (nodes.isEmpty()) {\n            LOGGER.warn(\"Unable to find jclouds node: {}\", jcloudsId);\n            for (ComputeMetadata n : computeService.listNodes()) {\n                LOGGER.info(\"Did find node: {}\", n);\n            }\n            throw new IllegalStateException(\"Unable to find node using jclouds: \" + jcloudsId);\n        }\n        NodeMetadata node = Iterables.getOnlyElement(nodes);\n        return node;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public String findSecurityGroup(String instanceId, String groupName) {\n        String vpcId = getVpcId(instanceId);\n\n        SecurityGroup found = null;\n        List<SecurityGroup> securityGroups = describeSecurityGroups(vpcId, groupName);\n        for (SecurityGroup sg : securityGroups) {\n            if (Objects.equal(vpcId, sg.getVpcId())) {\n                if (found != null) {\n                    throw new IllegalStateException(\"Duplicate security groups found\");\n                }\n                found = sg;\n            }\n        }\n        if (found == null) {\n            return null;\n        }\n        return found.getGroupId();\n    }\n\n    /**\n     * Gets the VPC id for the given instance.\n     *\n     * @param instanceId\n     *            instance we're checking\n     * @return vpc id, or null if not a vpc instance\n     */\n    String getVpcId(String instanceId) {\n        Instance awsInstance = describeInstance(instanceId);\n\n        String vpcId = awsInstance.getVpcId();\n        if (Strings.isNullOrEmpty(vpcId)) {\n            return null;\n        }\n\n        return vpcId;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public boolean canChangeInstanceSecurityGroups(String instanceId) {\n        return null != getVpcId(instanceId);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/aws/chaos/ASGChaosCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.client.aws.chaos;\n\nimport java.util.EnumSet;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.Instance;\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.netflix.simianarmy.GroupType;\nimport com.netflix.simianarmy.basic.chaos.BasicChaosMonkey;\nimport com.netflix.simianarmy.basic.chaos.BasicInstanceGroup;\nimport com.netflix.simianarmy.chaos.ChaosCrawler;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.tunable.TunableInstanceGroup;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * The Class ASGChaosCrawler. This will crawl for all available AutoScalingGroups associated with the AWS account.\n */\npublic class ASGChaosCrawler implements ChaosCrawler {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ASGChaosCrawler.class);\n\n    /**\n     * The key of the tag that set the aggression coefficient\n     */\n    private static final String CHAOS_MONKEY_AGGRESSION_COEFFICIENT_KEY = \"chaosMonkey.aggressionCoefficient\";\n\n    /**\n     * The group types Types.\n     */\n    public enum Types implements GroupType {\n\n        /** only crawls AutoScalingGroups. */\n        ASG;\n    }\n\n    /** The aws client. */\n    private final AWSClient awsClient;\n\n    /**\n     * Instantiates a new basic chaos crawler.\n     *\n     * @param awsClient\n     *            the aws client\n     */\n    public ASGChaosCrawler(AWSClient awsClient) {\n        this.awsClient = awsClient;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public EnumSet<?> groupTypes() {\n        return EnumSet.allOf(Types.class);\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<InstanceGroup> groups() {\n        return groups((String[]) null);\n    }\n\n    @Override\n    public List<InstanceGroup> groups(String... names) {\n        List<InstanceGroup> list = new LinkedList<InstanceGroup>();\n        \n        for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups(names)) {\n          \n            InstanceGroup ig = getInstanceGroup(asg, findAggressionCoefficient(asg));\n           \n            for (Instance inst : asg.getInstances()) {\n                ig.addInstance(inst.getInstanceId());\n            }\n            \n            list.add(ig);\n        }\n        return list;\n    }\n\n    /**\n     * Returns the desired InstanceGroup.  If there is no set aggression coefficient, then it\n     * returns the basic impl, otherwise it returns the tunable impl.\n     * @param asg The autoscaling group \n     * @return The appropriate {@link InstanceGroup}\n     */\n    protected InstanceGroup getInstanceGroup(AutoScalingGroup asg, double aggressionCoefficient) {\n      InstanceGroup instanceGroup;\n\n      // if coefficient is 1 then the BasicInstanceGroup is fine, otherwise use Tunable\n      if (aggressionCoefficient == 1.0) {\n          instanceGroup = new BasicInstanceGroup(asg.getAutoScalingGroupName(), Types.ASG, awsClient.region(), asg.getTags());\n      } else {\n        TunableInstanceGroup tunable = new TunableInstanceGroup(asg.getAutoScalingGroupName(), Types.ASG, awsClient.region(), asg.getTags());\n        tunable.setAggressionCoefficient(aggressionCoefficient);\n        \n        instanceGroup = tunable;\n      }\n      \n      return instanceGroup;\n    }\n    \n    /**\n     * Reads tags on AutoScalingGroup looking for the tag for the aggression coefficient \n     * and determines the coefficient value. The default value is 1 if there no tag or \n     * if the value in the tag is not a parsable number.\n     * \n     * @param asg The AutoScalingGroup that might have an aggression coefficient tag\n     * @return The set or default aggression coefficient.\n     */\n    protected double findAggressionCoefficient(AutoScalingGroup asg) {\n\n      List<TagDescription> tagDescriptions = asg.getTags();\n      \n      double aggression = 1.0;\n\n      for (TagDescription tagDescription : tagDescriptions) {\n\n        if ( CHAOS_MONKEY_AGGRESSION_COEFFICIENT_KEY.equalsIgnoreCase(tagDescription.getKey()) ) {\n          String value = tagDescription.getValue();\n\n          // prevent NPE on parseDouble\n          if (value == null) {\n            break;\n          }\n          \n          try {\n            aggression = Double.parseDouble(value);\n            LOGGER.info(\"Aggression coefficient of {} found for ASG {}\", value, asg.getAutoScalingGroupName());\n          } catch (NumberFormatException e) {\n            LOGGER.warn(\"Unparsable value of {} found in tag {} for ASG {}\", value, CHAOS_MONKEY_AGGRESSION_COEFFICIENT_KEY, asg.getAutoScalingGroupName());\n            aggression = 1.0;\n          }\n\n          // stop looking\n          break;\n        }\n      }\n\n      return aggression;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/aws/chaos/FilteringChaosCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.client.aws.chaos;\n\nimport com.google.common.base.Predicate;\nimport com.google.common.collect.Iterables;\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.chaos.ChaosCrawler;\n\nimport java.util.EnumSet;\nimport java.util.List;\n\n/**\n * The Class FilteringChaosCrawler. This will filter the result from ASGChaosCrawler for all available AutoScalingGroups associated with the AWS account based on requested filter.\n */\npublic class FilteringChaosCrawler implements ChaosCrawler {\n\n    private final ChaosCrawler crawler;\n    private final Predicate<? super InstanceGroup> predicate;\n\n    public FilteringChaosCrawler(ChaosCrawler crawler, Predicate<? super InstanceGroup> predicate) {\n        this.crawler = crawler;\n        this.predicate = predicate;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public EnumSet<?> groupTypes() {\n        return crawler.groupTypes();\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<InstanceGroup> groups() {\n        return filter(crawler.groups());\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public List<InstanceGroup> groups(String... names) {\n        return filter(crawler.groups(names));\n    }\n\n\n    /**\n     * Return the filtered list of InstanceGroups using the requested predicate. The filter is applied on the InstanceGroup retrieved from the ASGChaosCrawler class.\n     * @param list list of InstanceGroups result of the chaos crawler\n     * @return The appropriate {@link InstanceGroup}\n     */\n    protected List<InstanceGroup> filter(List<InstanceGroup> list) {\n        return Lists.newArrayList(Iterables.filter(list, predicate));\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/aws/chaos/TagPredicate.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.client.aws.chaos;\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.google.common.base.Predicate;\nimport com.google.common.collect.Iterables;\nimport com.netflix.simianarmy.chaos.ChaosCrawler;\n\n/**\n *  * The Class TagPredicate. This will apply the tag-key and the tag-value filter on the list of InstanceGroups .\n */\npublic class TagPredicate implements Predicate<ChaosCrawler.InstanceGroup> {\n\n    private final String key, value;\n\n    public TagPredicate(String key, String value) {\n        this.key = key;\n        this.value = value;\n    }\n\n    @Override\n    public boolean apply(ChaosCrawler.InstanceGroup instanceGroup) {\n        return Iterables.any(instanceGroup.tags(), new com.google.common.base.Predicate<TagDescription>() {\n            @Override\n            public boolean apply(TagDescription tagDescription) {\n                return tagDescription.getKey().equals(key) && tagDescription.getValue().equals(value);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/edda/EddaClient.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.client.edda;\n\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.client.MonkeyRestClient;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * The REST client to access Edda to get the history of a cloud resource.\n */\npublic class EddaClient extends MonkeyRestClient {\n    private static final Logger LOGGER = LoggerFactory.getLogger(EddaClient.class);\n\n    private final MonkeyConfiguration config;\n    /**\n     * Constructor.\n     * @param timeout the timeout in milliseconds\n     * @param maxRetries the max number of retries\n     * @param retryInterval the interval in milliseconds between retries\n     * @param config the monkey configuration\n     */\n    public EddaClient(int timeout, int maxRetries, int retryInterval, MonkeyConfiguration config) {\n        super(timeout, maxRetries, retryInterval);\n        this.config = config;\n    }\n\n    @Override\n    public String getBaseUrl(String region) {\n        Validate.notEmpty(region);\n        String baseUrl = config.getStr(\"simianarmy.janitor.edda.endpoint.\" + region);\n        if (StringUtils.isBlank(baseUrl)) {\n            LOGGER.error(String.format(\"No endpoint of Edda is found for region %s.\", region));\n        }\n        return baseUrl;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/vsphere/PropertyBasedTerminationStrategy.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport java.rmi.RemoteException;\r\n\r\nimport com.netflix.simianarmy.MonkeyConfiguration;\r\nimport com.vmware.vim25.mo.VirtualMachine;\r\n\r\n/**\r\n * Terminates a VirtualMachine by setting the named property and resetting it.\r\n *\r\n * The following properties can be overridden in the client.properties\r\n * simianarmy.client.vsphere.terminationStrategy.property.name  = PROPERTY_NAME\r\n * simianarmy.client.vsphere.terminationStrategy.property.value = PROPERTY_VALUE\r\n *\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class PropertyBasedTerminationStrategy implements TerminationStrategy {\r\n    private final String propertyName;\r\n    private final String propertyValue;\r\n\r\n    /**\r\n     * Reads property name <code>simianarmy.client.vsphere.terminationStrategy.property.name</code>\r\n     * (default: Force Boot) and value <code>simianarmy.client.vsphere.terminationStrategy.property.value</code>\r\n     * (default: server) from config.\r\n     */\r\n    public PropertyBasedTerminationStrategy(MonkeyConfiguration config) {\r\n        this.propertyName = config.getStrOrElse(\r\n                \"simianarmy.client.vsphere.terminationStrategy.property.name\", \"Force Boot\");\r\n        this.propertyValue = config.getStrOrElse(\r\n                \"simianarmy.client.vsphere.terminationStrategy.property.value\", \"server\");\r\n    }\r\n\r\n    @Override\r\n    public void terminate(VirtualMachine virtualMachine) throws RemoteException {\r\n        virtualMachine.setCustomValue(getPropertyName(), getPropertyValue());\r\n        virtualMachine.resetVM_Task();\r\n    }\r\n\r\n    public String getPropertyName() {\r\n        return propertyName;\r\n    }\r\n\r\n    public String getPropertyValue() {\r\n        return propertyValue;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/vsphere/TerminationStrategy.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport java.rmi.RemoteException;\r\n\r\nimport com.vmware.vim25.mo.VirtualMachine;\r\n\r\n/**\r\n * Abstracts the concrete way a VirtualMachine is terminated. Implement this to fit to your infrastructure.\r\n *\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic interface TerminationStrategy {\r\n    /**\r\n     * Terminate the given VirtualMachine.\r\n     */\r\n    void terminate(VirtualMachine virtualMachine) throws RemoteException;\r\n}\r\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/vsphere/VSphereClient.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport java.rmi.RemoteException;\r\nimport java.util.List;\r\n\r\nimport com.amazonaws.AmazonServiceException;\r\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\r\nimport com.netflix.simianarmy.client.aws.AWSClient;\r\nimport com.vmware.vim25.mo.VirtualMachine;\r\n\r\n/**\r\n * This client describes the VSphere folders as AutoScalingGroup's containing the virtual machines that are directly in\r\n * that folder. The hierarchy is flattened this way. And it can terminate these VMs with the configured\r\n * TerminationStrategy.\r\n *\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class VSphereClient extends AWSClient {\r\n//    private static final Logger LOGGER = LoggerFactory.getLogger(VSphereClient.class);\r\n\r\n    private final TerminationStrategy terminationStrategy;\r\n    private final VSphereServiceConnection connection;\r\n\r\n    /**\r\n     * Create the specific Client from the given strategy and connection.\r\n     */\r\n    public VSphereClient(TerminationStrategy terminationStrategy, VSphereServiceConnection connection) {\r\n        super(\"region-\" + connection.getUrl());\r\n        this.terminationStrategy = terminationStrategy;\r\n        this.connection = connection;\r\n    }\r\n\r\n    @Override\r\n    public List<AutoScalingGroup> describeAutoScalingGroups(String... names) {\r\n        final VSphereGroups groups = new VSphereGroups();\r\n\r\n        try {\r\n            connection.connect();\r\n\r\n            for (VirtualMachine virtualMachine : connection.describeVirtualMachines()) {\r\n                String instanceId = virtualMachine.getName();\r\n                String groupName = virtualMachine.getParent().getName();\r\n\r\n                boolean shouldAddNamedGroup = true;\r\n                if (names != null) {\r\n                    // TODO need to implement this feature!!!\r\n                    throw new RuntimeException(\"This feature (selecting groups by name) is not implemented yet\");\r\n                }\r\n\r\n                if (shouldAddNamedGroup) {\r\n                    groups.addInstance(instanceId, groupName);\r\n                }\r\n            }\r\n        } finally {\r\n            connection.disconnect();\r\n        }\r\n\r\n        return groups.asList();\r\n    }\r\n\r\n    @Override\r\n    /**\r\n     * reinstall the given instance. If it is powered down this will be ignored and the\r\n     * reinstall occurs the next time the machine is powered up.\r\n     */\r\n    public void terminateInstance(String instanceId) {\r\n        try {\r\n            connection.connect();\r\n\r\n            VirtualMachine virtualMachine = connection.getVirtualMachineById(instanceId);\r\n            this.terminationStrategy.terminate(virtualMachine);\r\n        } catch (RemoteException e) {\r\n            throw new AmazonServiceException(\"cannot destroy & recreate \" + instanceId, e);\r\n        } finally {\r\n            connection.disconnect();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/vsphere/VSphereContext.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport com.netflix.simianarmy.MonkeyConfiguration;\r\nimport com.netflix.simianarmy.basic.BasicChaosMonkeyContext;\r\n\r\n/**\r\n * This Context extends the BasicContext in order to provide a different client: the VSphereClient.\r\n *\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class VSphereContext extends BasicChaosMonkeyContext {\r\n    @Override\r\n    protected void createClient() {\r\n        MonkeyConfiguration config = configuration();\r\n        final PropertyBasedTerminationStrategy terminationStrategy = new PropertyBasedTerminationStrategy(config);\r\n        final VSphereServiceConnection connection = new VSphereServiceConnection(config);\r\n        final VSphereClient client = new VSphereClient(terminationStrategy, connection);\r\n        setCloudClient(client);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/vsphere/VSphereGroups.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport java.util.ArrayList;\r\nimport java.util.Collections;\r\nimport java.util.Comparator;\r\nimport java.util.HashMap;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\r\nimport com.amazonaws.services.autoscaling.model.Instance;\r\n\r\n/**\r\n * Wraps the creation and grouping of Instance's in AutoScalingGroup's.\r\n *\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\nclass VSphereGroups {\r\n    private final Map<String, AutoScalingGroup> map = new HashMap<String, AutoScalingGroup>();\r\n\r\n    /**\r\n     * Get all AutoScalingGroup's that have been added.\r\n     */\r\n    public List<AutoScalingGroup> asList() {\r\n        ArrayList<AutoScalingGroup> list = new ArrayList<AutoScalingGroup>(map.values());\r\n        Collections.sort(list, new Comparator<AutoScalingGroup>() {\r\n            @Override\r\n            public int compare(AutoScalingGroup o1, AutoScalingGroup o2) {\r\n                return o1.getAutoScalingGroupName().compareTo(o2.getAutoScalingGroupName());\r\n            }\r\n        });\r\n        return list;\r\n    }\r\n\r\n    /**\r\n     * Add the given instance to the named group.\r\n     */\r\n    public void addInstance(final String instanceId, final String groupName) {\r\n        if (!map.containsKey(groupName)) {\r\n            final AutoScalingGroup asg = new AutoScalingGroup();\r\n            asg.setAutoScalingGroupName(groupName);\r\n            map.put(groupName, asg);\r\n        }\r\n\r\n        final AutoScalingGroup asg = map.get(groupName);\r\n        Instance instance = new Instance();\r\n        instance.setInstanceId(instanceId);\r\n\r\n        asg.getInstances().add(instance);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/client/vsphere/VSphereServiceConnection.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport java.net.MalformedURLException;\r\nimport java.net.URL;\r\nimport java.rmi.RemoteException;\r\nimport java.util.Arrays;\r\n\r\nimport com.amazonaws.AmazonServiceException;\r\nimport com.netflix.simianarmy.MonkeyConfiguration;\r\nimport com.vmware.vim25.InvalidProperty;\r\nimport com.vmware.vim25.RuntimeFault;\r\nimport com.vmware.vim25.mo.InventoryNavigator;\r\nimport com.vmware.vim25.mo.ManagedEntity;\r\nimport com.vmware.vim25.mo.ServiceInstance;\r\nimport com.vmware.vim25.mo.VirtualMachine;\r\n\r\n/**\r\n * Wraps the connection to VSphere and handles the raw service calls.\r\n *\r\n * The following properties can be overridden in the client.properties\r\n * simianarmy.client.vsphere.url                                = https://YOUR_VSPHERE_SERVER/sdk\r\n * simianarmy.client.vsphere.username                           = YOUR_SERVICE_ACCOUNT_USERNAME\r\n * simianarmy.client.vsphere.password                           = YOUR_SERVICE_ACCOUNT_PASSWORD\r\n *\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class VSphereServiceConnection {\r\n    /** The type of managedEntity we operate on are virtual machines. */\r\n    public static final String VIRTUAL_MACHINE_TYPE_NAME = \"VirtualMachine\";\r\n\r\n    /** The username that is used to connect to VSpehere Center. */\r\n    private String username = null;\r\n\r\n    /** The password that is used to connect to VSpehere Center. */\r\n    private String password = null;\r\n\r\n    /** The url that is used to connect to VSpehere Center. */\r\n    private String url = null;\r\n\r\n    /** The ServiceInstance that is used to issue multiple requests to VSpehere Center. */\r\n    private ServiceInstance service = null;\r\n\r\n    /**\r\n     * Constructor.\r\n     */\r\n    public VSphereServiceConnection(MonkeyConfiguration config) {\r\n        this.url = config.getStr(\"simianarmy.client.vsphere.url\");\r\n        this.username = config.getStr(\"simianarmy.client.vsphere.username\");\r\n        this.password = config.getStr(\"simianarmy.client.vsphere.password\");\r\n    }\r\n\r\n    /** disconnect from the service if not already disconnected. */\r\n    public void disconnect() {\r\n        if (service != null) {\r\n            service.getServerConnection().logout();\r\n            service = null;\r\n        }\r\n    }\r\n\r\n    /** connect to the service if not already connected. */\r\n    public void connect() throws AmazonServiceException {\r\n        try {\r\n            if (service == null) {\r\n                service = new ServiceInstance(new URL(url), username, password, true);\r\n            }\r\n        } catch (RemoteException e) {\r\n            throw new AmazonServiceException(\"cannot connect to VSphere\", e);\r\n        } catch (MalformedURLException e) {\r\n            throw new AmazonServiceException(\"cannot connect to VSphere\", e);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Gets the named VirtualMachine.\r\n     */\r\n    public VirtualMachine getVirtualMachineById(String instanceId) throws RemoteException {\r\n        InventoryNavigator inventoryNavigator = getInventoryNavigator();\r\n        VirtualMachine virtualMachine = (VirtualMachine) inventoryNavigator.searchManagedEntity(\r\n                VIRTUAL_MACHINE_TYPE_NAME, instanceId);\r\n\r\n        return virtualMachine;\r\n    }\r\n\r\n    /**\r\n     * Return all VirtualMachines from VSpehere Center.\r\n     *\r\n     * @throws AmazonServiceException\r\n     *             If there is any communication error or if no VirtualMachine's are found. */\r\n    public VirtualMachine[] describeVirtualMachines() throws AmazonServiceException {\r\n        ManagedEntity[] mes = null;\r\n\r\n        try {\r\n            mes = getInventoryNavigator().searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME);\r\n        } catch (InvalidProperty e) {\r\n            throw new AmazonServiceException(\"cannot query VSphere\", e);\r\n        } catch (RuntimeFault e) {\r\n            throw new AmazonServiceException(\"cannot query VSphere\", e);\r\n        } catch (RemoteException e) {\r\n            throw new AmazonServiceException(\"cannot query VSphere\", e);\r\n        }\r\n\r\n        if (mes == null || mes.length == 0) {\r\n            throw new AmazonServiceException(\r\n                    \"vsphere returned zero entities of type \\\"\"\r\n                            + VIRTUAL_MACHINE_TYPE_NAME + \"\\\"\"\r\n                    );\r\n        } else {\r\n            return Arrays.copyOf(mes, mes.length, VirtualMachine[].class);\r\n        }\r\n    }\r\n\r\n    protected InventoryNavigator getInventoryNavigator() {\r\n        return new InventoryNavigator(service.getRootFolder());\r\n    }\r\n\r\n    public String getUsername() {\r\n        return username;\r\n    }\r\n    public String getPassword() {\r\n        return password;\r\n    }\r\n    public String getUrl() {\r\n        return url;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/AutoScalingGroup.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport com.google.common.collect.Lists;\nimport org.apache.commons.lang.Validate;\n\nimport java.util.Collection;\nimport java.util.Collections;\n\n/**\n * The class implementing the auto scaling groups.\n */\npublic class AutoScalingGroup {\n    private final String name;\n    private final Collection<String> instances = Lists.newArrayList();\n    private boolean isSuspended;\n\n    /**\n     * Constructor.\n     * @param name\n     *          the name of the auto scaling group\n     * @param instances\n     *          the instance ids in the auto scaling group\n     */\n    public AutoScalingGroup(String name, String... instances) {\n        Validate.notNull(instances);\n        this.name = name;\n        for (String instance : instances) {\n            this.instances.add(instance);\n        }\n        this.isSuspended = false;\n    }\n\n    /**\n     * Gets the name of the auto scaling group.\n     * @return\n     *      the name of the auto scaling group\n     */\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * * Gets the instances of the auto scaling group.\n     * @return\n     *    the instances of the auto scaling group\n     */\n    public Collection<String> getInstances() {\n        return Collections.unmodifiableCollection(instances);\n    }\n\n    /**\n     * Gets the flag to indicate whether the ASG is suspended.\n     * @return true if the ASG is suspended, false otherwise\n     */\n    public boolean isSuspended() {\n        return isSuspended;\n    }\n\n    /**\n     * Sets the flag to indicate whether the ASG is suspended.\n     * @param suspended true if the ASG is suspended, false otherwise\n     */\n    public void setSuspended(boolean suspended) {\n        isSuspended = suspended;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/Cluster.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.google.common.collect.Sets;\nimport org.apache.commons.lang.StringUtils;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * The class implementing clusters. Cluster is the basic unit of conformity check. It can be a single ASG or\n * a group of ASGs that belong to the same application, for example, a cluster in the Asgard deployment system.\n */\npublic class Cluster {\n    public static final String OWNER_EMAIL = \"ownerEmail\";\n    public static final String CLUSTER = \"cluster\";\n    public static final String REGION = \"region\";\n    public static final String IS_CONFORMING = \"isConforming\";\n    public static final String IS_OPTEDOUT = \"isOptedOut\";\n    public static final String UPDATE_TIMESTAMP = \"updateTimestamp\";\n    public static final String EXCLUDED_RULES = \"excludedRules\";\n    public static final String CONFORMITY_RULES = \"conformityRules\";\n    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern(\"yyyy-MM-dd'T'HH:mm:ss\");\n\n    private final String name;\n    private final Collection<AutoScalingGroup> autoScalingGroups = Lists.newArrayList();\n    private final String region;\n    private String ownerEmail;\n    private Date updateTime;\n    private final Map<String, Conformity> conformities = Maps.newHashMap();\n    private final Collection<String> excludedConformityRules = Sets.newHashSet();\n    private boolean isConforming;\n    private boolean isOptOutOfConformity;\n    private final Set<String> soloInstances = Sets.newHashSet();\n\n    /**\n     * Constructor.\n     * @param name\n     *          the name of the cluster\n     * @param autoScalingGroups\n     *          the auto scaling groups in the cluster\n     */\n    public Cluster(String name, String region, AutoScalingGroup... autoScalingGroups) {\n        Validate.notNull(name);\n        Validate.notNull(region);\n        Validate.notNull(autoScalingGroups);\n        this.name = name;\n        this.region = region;\n        for (AutoScalingGroup asg : autoScalingGroups) {\n            this.autoScalingGroups.add(asg);\n        }\n    }\n\n    /**\n     * Constructor.\n     * @param name\n     *          the name of the cluster\n     * @param soloInstances\n     *          the list of all instances\n     */\n    public Cluster(String name, String region, Set<String> soloInstances) {\n        Validate.notNull(name);\n        Validate.notNull(region);\n        Validate.notNull(soloInstances);\n        this.name = name;\n        this.region = region;\n        for (String soleInstance : soloInstances) {\n            this.soloInstances.add(soleInstance);\n        }\n    }\n\n    /**\n     * Gets the name of the cluster.\n     * @return\n     *      the name of the cluster\n     */\n    public String getName() {\n        return name;\n    }\n\n    /**\n     * Gets the region of the cluster.\n     * @return\n     *      the region of the cluster\n     */\n    public String getRegion() {\n        return region;\n    }\n\n    /**\n     * * Gets the auto scaling groups of the auto scaling group.\n     * @return\n     *    the auto scaling groups in the cluster\n     */\n    public Collection<AutoScalingGroup> getAutoScalingGroups() {\n        return Collections.unmodifiableCollection(autoScalingGroups);\n    }\n\n    /**\n     * Gets the owner email of the cluster.\n     * @return\n     *      the owner email of the cluster\n     */\n    public String getOwnerEmail() {\n        return ownerEmail;\n    }\n\n    /**\n     * Sets the owner email of the cluster.\n     * @param ownerEmail\n     *              the owner email of the cluster\n     */\n    public void setOwnerEmail(String ownerEmail) {\n        this.ownerEmail = ownerEmail;\n    }\n\n    /**\n     * Gets the update time of the cluster.\n     * @return\n     *      the update time of the cluster\n     */\n    public Date getUpdateTime() {\n        return new Date(updateTime.getTime());\n    }\n\n    /**\n     * Sets the update time of the cluster.\n     * @param updateTime\n     *              the update time of the cluster\n     */\n    public void setUpdateTime(Date updateTime) {\n        this.updateTime = new Date(updateTime.getTime());\n    }\n\n    /**\n     * Gets all conformity check information of the cluster.\n     * @return\n     *      all conformity check information of the cluster\n     */\n    public Collection<Conformity> getConformties() {\n        return conformities.values();\n    }\n\n    /**\n     * Gets the conformity information for a conformity rule.\n     * @param rule\n     *          the conformity rule\n     * @return\n     *          the conformity for the rule\n     */\n    public Conformity getConformity(ConformityRule rule) {\n        Validate.notNull(rule);\n        return conformities.get(rule.getName());\n    }\n\n    /**\n     * Updates the cluster with a new conformity check result.\n     * @param conformity\n     *          the conformity to update\n     * @return\n     *          the cluster itself\n     *\n     */\n    public Cluster updateConformity(Conformity conformity) {\n        Validate.notNull(conformity);\n        conformities.put(conformity.getRuleId(), conformity);\n        return this;\n    }\n\n    /**\n     * Clears the conformity check results.\n     */\n    public void clearConformities() {\n        conformities.clear();\n    }\n\n    /**\n     * Gets the boolean flag to indicate whether the cluster is conforming to\n     * all non-excluded conformity rules.\n     * @return\n     *      true if the cluster is conforming against all non-excluded rules,\n     *      false otherwise\n     */\n    public boolean isConforming() {\n        return isConforming;\n    }\n\n    /**\n     * Sets the boolean flag to indicate whether the cluster is conforming to\n     * all non-excluded conformity rules.\n     * @param conforming\n     *      true if the cluster is conforming against all non-excluded rules,\n     *      false otherwise\n     */\n    public void setConforming(boolean conforming) {\n        isConforming = conforming;\n    }\n\n    /**\n     * Gets names of all excluded conformity rules for this cluster.\n     * @return\n     *      names of all excluded conformity rules for this cluster\n     */\n    public Collection<String> getExcludedRules() {\n        return Collections.unmodifiableCollection(excludedConformityRules);\n    }\n\n    /**\n     * Excludes rules for the cluster.\n     * @param ruleIds\n     *          the rule ids to exclude\n     * @return\n     *          the cluster itself\n     */\n    public Cluster excludeRules(String... ruleIds) {\n        Validate.notNull(ruleIds);\n        for (String ruleId : ruleIds) {\n            Validate.notNull(ruleId);\n            excludedConformityRules.add(ruleId.trim());\n        }\n        return this;\n    }\n\n    /**\n     * Gets the flag to indicate whether the cluster is opted out of Conformity monkey.\n     * @return true if the cluster is not handled by Conformity monkey, false otherwise\n     */\n    public boolean isOptOutOfConformity() {\n        return isOptOutOfConformity;\n    }\n\n    /**\n     * Sets the flag to indicate whether the cluster is opted out of Conformity monkey.\n     * @param optOutOfConformity\n     *            true if the cluster is not handled by Conformity monkey, false otherwise\n     */\n    public void setOptOutOfConformity(boolean optOutOfConformity) {\n        isOptOutOfConformity = optOutOfConformity;\n    }\n\n    /**\n     * Gets a map from fields of resources to corresponding values. Values are represented\n     * as Strings so they can be displayed or stored in databases like SimpleDB.\n     * @return a map from field name to field value\n     */\n    public Map<String, String> getFieldToValueMap() {\n        Map<String, String> map = Maps.newHashMap();\n        putToMapIfNotNull(map, CLUSTER, name);\n        putToMapIfNotNull(map, REGION, region);\n        putToMapIfNotNull(map, OWNER_EMAIL, ownerEmail);\n        putToMapIfNotNull(map, UPDATE_TIMESTAMP, String.valueOf(DATE_FORMATTER.print(updateTime.getTime())));\n        putToMapIfNotNull(map, IS_CONFORMING, String.valueOf(isConforming));\n        putToMapIfNotNull(map, IS_OPTEDOUT, String.valueOf(isOptOutOfConformity));\n        putToMapIfNotNull(map, EXCLUDED_RULES, StringUtils.join(excludedConformityRules, \",\"));\n        List<String> ruleIds = Lists.newArrayList();\n        for (Conformity conformity : conformities.values()) {\n            map.put(conformity.getRuleId(), StringUtils.join(conformity.getFailedComponents(), \",\"));\n            ruleIds.add(conformity.getRuleId());\n        }\n        putToMapIfNotNull(map, CONFORMITY_RULES, StringUtils.join(ruleIds, \",\"));\n        return map;\n    }\n\n    /**\n     * Parse a map from field name to value to a cluster.\n     * @param fieldToValue the map from field name to value\n     * @return the cluster that is de-serialized from the map\n     */\n    public static Cluster parseFieldToValueMap(Map<String, String> fieldToValue) {\n        Validate.notNull(fieldToValue);\n        Cluster cluster = new Cluster(fieldToValue.get(CLUSTER),\n                fieldToValue.get(REGION));\n        cluster.setOwnerEmail(fieldToValue.get(OWNER_EMAIL));\n        cluster.setConforming(Boolean.parseBoolean(fieldToValue.get(IS_CONFORMING)));\n        cluster.setOptOutOfConformity(Boolean.parseBoolean(fieldToValue.get(IS_OPTEDOUT)));\n        cluster.excludeRules(StringUtils.split(fieldToValue.get(EXCLUDED_RULES), \",\"));\n        cluster.setUpdateTime(new Date(DATE_FORMATTER.parseDateTime(fieldToValue.get(UPDATE_TIMESTAMP)).getMillis()));\n        for (String ruleId : StringUtils.split(fieldToValue.get(CONFORMITY_RULES), \",\")) {\n            cluster.updateConformity(new Conformity(ruleId,\n                    Lists.newArrayList(StringUtils.split(fieldToValue.get(ruleId), \",\"))));\n        }\n        return cluster;\n    }\n\n    private static void putToMapIfNotNull(Map<String, String> map, String key, String value) {\n        Validate.notNull(map);\n        Validate.notNull(key);\n        if (value != null) {\n            map.put(key, value);\n        }\n    }\n\n    public Set<String> getSoloInstances() {\n        return Collections.unmodifiableSet(soloInstances);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/ClusterCrawler.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport java.util.List;\n\n/**\n * The interface of the crawler for Conformity Monkey to get the cluster information.\n */\npublic interface ClusterCrawler {\n\n    /**\n     * Gets the up to date information for a collection of clusters. When the input argument is null\n     * or empty, the method returns all clusters.\n     *\n     * @param clusterNames\n     *          the cluster names\n     * @return the list of clusters\n     */\n    List<Cluster> clusters(String... clusterNames);\n\n    /**\n     * Gets the owner email for a cluster to set the ownerEmail field when crawl.\n     * @param cluster\n     *          the cluster\n     * @return the owner email of the cluster\n     */\n    String getOwnerEmailForCluster(Cluster cluster);\n\n    /**\n     * Updates the excluded conformity rules for the given cluster.\n     * @param cluster\n     */\n    void updateExcludedConformityRules(Cluster cluster);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/Conformity.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport com.google.common.collect.Lists;\nimport org.apache.commons.lang.Validate;\n\nimport java.util.Collection;\nimport java.util.Collections;\n\n/**\n * The class defining the result of a conformity check.\n */\npublic class Conformity {\n\n    private final String ruleId;\n    private final Collection<String> failedComponents = Lists.newArrayList();\n\n    /**\n     * Constructor.\n     * @param ruleId\n     *          the conformity rule id\n     * @param failedComponents\n     *          the components that cause the conformity check to fail, if there is\n     *          no failed components, it means the conformity check passes.\n     */\n    public Conformity(String ruleId, Collection<String> failedComponents) {\n        Validate.notNull(ruleId);\n        Validate.notNull(failedComponents);\n        this.ruleId = ruleId;\n        for (String failedComponent : failedComponents) {\n            this.failedComponents.add(failedComponent);\n        }\n    }\n\n    /**\n     * Gets the conformity rule id.\n     * @return\n     *      the conformity rule id\n     */\n    public String getRuleId() {\n        return ruleId;\n    }\n\n    /**\n     * Gets the components that cause the conformity check to fail.\n     * @return\n     *      the components that cause the conformity check to fail\n     */\n    public Collection<String> getFailedComponents() {\n        return Collections.unmodifiableCollection(failedComponents);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/ConformityClusterTracker.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport java.util.List;\n\n/**\n * The interface that defines the tracker to manage clusters for Conformity monkey to use.\n */\npublic interface ConformityClusterTracker {\n    /**\n     * Adds a cluster to the tracker. If the cluster with the same name already exists,\n     * the method updates the record with the cluster parameter.\n     * @param cluster\n     *          the cluster to add or update\n     */\n    void addOrUpdate(Cluster cluster);\n\n    /**\n     * Gets the list of clusters in a list of regions.\n     * @param regions\n     *      the regions of the clusters, when the parameter is null or empty, the method returns\n     *      clusters from all regions\n     * @return list of clusters in the given regions\n     */\n    List<Cluster> getAllClusters(String... regions);\n\n    /**\n     * Gets the list of non-conforming clusters in a list of regions.\n     * @param regions the regions of the clusters, when the parameter is null or empty, the method returns\n     * clusters from all regions\n     * @return list of clusters in the given regions\n     */\n    List<Cluster> getNonconformingClusters(String... regions);\n\n    /**\n     * Gets the cluster with a specific name from .\n     * @param name the cluster name\n     * @param region the region of the cluster\n     * @return the cluster with the name\n     */\n    Cluster getCluster(String name, String region);\n\n\n    /**\n     * Deletes a list of clusters from the tracker.\n     * @param clusters the list of clusters to delete. The parameter cannot be null. If it is empty,\n     *                 no cluster is deleted.\n     */\n    void deleteClusters(Cluster... clusters);\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/ConformityEmailBuilder.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport com.netflix.simianarmy.AbstractEmailBuilder;\n\nimport java.util.Collection;\nimport java.util.Map;\n\n/** The abstract class for building Conformity monkey email notifications. */\npublic abstract class ConformityEmailBuilder extends AbstractEmailBuilder {\n\n    /**\n     * Sets the map from an owner email to the clusters that belong to the owner\n     * and need to send notifications for.\n     * @param emailToClusters the map from owner email to the owned clusters\n     * @param rules all conformity rules that are used to find the description of each rule to display\n     */\n    public abstract void setEmailToClusters(Map<String, Collection<Cluster>> emailToClusters,\n            Collection<ConformityRule> rules);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/ConformityEmailNotifier.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.aws.AWSEmailNotifier;\nimport org.apache.commons.lang.Validate;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * The email notifier implemented for Janitor Monkey.\n */\npublic class ConformityEmailNotifier  extends AWSEmailNotifier {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ConformityEmailNotifier.class);\n    private static final String UNKNOWN_EMAIL = \"UNKNOWN\";\n\n    private final Collection<String> regions = Lists.newArrayList();\n    private final String defaultEmail;\n    private final List<String> ccEmails = Lists.newArrayList();\n    private final ConformityClusterTracker clusterTracker;\n    private final ConformityEmailBuilder emailBuilder;\n    private final String sourceEmail;\n    private final Map<String, Collection<Cluster>> invalidEmailToClusters = Maps.newHashMap();\n    private final Collection<ConformityRule> rules = Lists.newArrayList();\n    private final int openHour;\n    private final int closeHour;\n\n    /**\n     * The Interface Context.\n     */\n    public interface Context {\n        /**\n         * Gets the Amazon Simple Email Service client.\n         * @return the Amazon Simple Email Service client\n         */\n        AmazonSimpleEmailServiceClient sesClient();\n\n        /**\n         * Gets the open hour the email notifications are sent.\n         * @return\n         *      the open hour the email notifications are sent\n         */\n        int openHour();\n\n        /**\n         * Gets the close hour the email notifications are sent.\n         * @return\n         *      the close hour the email notifications are sent\n         */\n        int closeHour();\n\n        /**\n         * Gets the source email the notifier uses to send email.\n         * @return the source email\n         */\n        String sourceEmail();\n\n        /**\n         * Gets the default email the notifier sends to when there is no owner specified for a cluster.\n         * @return the default email\n         */\n        String defaultEmail();\n\n        /**\n         * Gets the regions the notifier is running in.\n         * @return the regions the notifier is running in.\n         */\n        Collection<String> regions();\n\n        /** Gets the Conformity Monkey's cluster tracker.\n         * @return the Conformity Monkey's cluster tracker\n         */\n        ConformityClusterTracker clusterTracker();\n\n        /** Gets the Conformity email builder.\n         * @return the Conformity email builder\n         */\n        ConformityEmailBuilder emailBuilder();\n\n        /** Gets the cc email addresses.\n         * @return the cc email addresses\n         */\n        String[] ccEmails();\n\n        /**\n         * Gets all the conformity rules.\n         * @return all conformity rules.\n         */\n        Collection<ConformityRule> rules();\n    }\n\n    /**\n     * Constructor.\n     * @param ctx the context.\n     */\n    public ConformityEmailNotifier(Context ctx) {\n        super(ctx.sesClient());\n        this.openHour = ctx.openHour();\n        this.closeHour = ctx.closeHour();\n        for (String region : ctx.regions()) {\n            this.regions.add(region);\n        }\n        this.defaultEmail = ctx.defaultEmail();\n        this.clusterTracker = ctx.clusterTracker();\n        this.emailBuilder = ctx.emailBuilder();\n        String[] ctxCCs = ctx.ccEmails();\n        if (ctxCCs != null) {\n            for (String ccEmail : ctxCCs) {\n                this.ccEmails.add(ccEmail);\n            }\n        }\n        this.sourceEmail = ctx.sourceEmail();\n        Validate.notNull(ctx.rules());\n        for (ConformityRule rule : ctx.rules()) {\n            rules.add(rule);\n        }\n    }\n\n    /**\n     * Gets all the clusters that are not conforming and sends email notifications to the owners.\n     */\n    public void sendNotifications() {\n        int currentHour = DateTime.now().getHourOfDay();\n        if (currentHour < openHour || currentHour > closeHour) {\n            LOGGER.info(\"It is not the time for Conformity Monkey to send notifications. You can change \"\n                    + \"simianarmy.conformity.notification.openHour and simianarmy.conformity.notification.openHour\"\n                    + \" to make it work at this hour.\");\n            return;\n        }\n\n        validateEmails();\n        Map<String, Collection<Cluster>> emailToClusters = Maps.newHashMap();\n        for (Cluster cluster : clusterTracker.getNonconformingClusters(regions.toArray(new String[regions.size()]))) {\n            if (cluster.isOptOutOfConformity()) {\n                LOGGER.info(String.format(\"Cluster %s is opted out of Conformity Monkey so no notification is sent.\",\n                        cluster.getName()));\n                continue;\n            }\n            if (!cluster.isConforming()) {\n                String email = cluster.getOwnerEmail();\n                if (!isValidEmail(email)) {\n                    if (defaultEmail != null) {\n                        LOGGER.info(String.format(\"Email %s is not valid, send to the default email address %s\",\n                                email, defaultEmail));\n                        putEmailAndCluster(emailToClusters, defaultEmail, cluster);\n                    } else {\n                        if (email == null) {\n                            email = UNKNOWN_EMAIL;\n                        }\n                        LOGGER.info(String.format(\"Email %s is not valid and default email is not set for cluster %s\",\n                                email, cluster.getName()));\n                        putEmailAndCluster(invalidEmailToClusters, email, cluster);\n                    }\n                } else {\n                    putEmailAndCluster(emailToClusters, email, cluster);\n                }\n            } else {\n                LOGGER.debug(String.format(\"Cluster %s is conforming so no notification needs to be sent.\",\n                        cluster.getName()));\n            }\n        }\n        emailBuilder.setEmailToClusters(emailToClusters, rules);\n        for (Map.Entry<String, Collection<Cluster>> entry : emailToClusters.entrySet()) {\n            String email = entry.getKey();\n            String emailBody = emailBuilder.buildEmailBody(email);\n            String subject = buildEmailSubject(email);\n            sendEmail(email, subject, emailBody);\n            for (Cluster cluster : entry.getValue()) {\n                LOGGER.debug(String.format(\"Notification is sent for cluster %s to %s\", cluster.getName(), email));\n            }\n            LOGGER.info(String.format(\"Email notification has been sent to %s for %d clusters.\",\n                    email, entry.getValue().size()));\n        }\n    }\n\n\n    @Override\n    public String buildEmailSubject(String to) {\n        return String.format(\"Conformity Monkey Notification for %s\", to);\n    }\n\n    @Override\n    public String[] getCcAddresses(String to) {\n        return ccEmails.toArray(new String[ccEmails.size()]);\n    }\n\n    @Override\n    public String getSourceAddress(String to) {\n        return sourceEmail;\n    }\n\n    private void validateEmails() {\n        if (defaultEmail != null) {\n            Validate.isTrue(isValidEmail(defaultEmail), String.format(\"Default email %s is invalid\", defaultEmail));\n        }\n        if (ccEmails != null) {\n            for (String ccEmail : ccEmails) {\n                Validate.isTrue(isValidEmail(ccEmail), String.format(\"CC email %s is invalid\", ccEmail));\n            }\n        }\n    }\n\n    private void putEmailAndCluster(Map<String, Collection<Cluster>> map, String email, Cluster cluster) {\n        Collection<Cluster> clusters = map.get(email);\n        if (clusters == null) {\n            clusters = Lists.newArrayList();\n            map.put(email, clusters);\n        }\n        clusters.add(cluster);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/ConformityMonkey.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.MonkeyType;\n\nimport java.util.Collection;\n\n/**\n * The abstract class for Conformity Monkey.\n */\npublic abstract class ConformityMonkey extends Monkey {\n\n    /**\n     * The Interface Context.\n     */\n    public interface Context extends Monkey.Context {\n\n        /**\n         * Configuration.\n         *\n         * @return the monkey configuration\n         */\n        MonkeyConfiguration configuration();\n\n        /**\n         * Crawler that gets information of all clusters for conformity check.\n         * @return all clusters for conformity check\n         */\n        ClusterCrawler clusterCrawler();\n\n        /**\n         * Conformity rule engine.\n         * @return the Conformity rule engine\n         */\n        ConformityRuleEngine ruleEngine();\n\n\n        /**\n         * Email notifier used to send notifications by the Conformity monkey.\n         * @return the email notifier\n         */\n        ConformityEmailNotifier emailNotifier();\n\n        /**\n         * The regions the monkey is running in.\n         * @return the regions the monkey is running in.\n         */\n        Collection<String> regions();\n\n        /**\n         * The tracker of the clusters for conformity monkey to check.\n         * @return the tracker of the clusters for conformity monkey to check.\n         */\n        ConformityClusterTracker clusterTracker();\n\n        /**\n         * Gets the flag to indicate whether the monkey is leashed.\n         * @return true if the monkey is leashed and does not make real change or send notifications to\n         * cluster owners, false otherwise.\n         */\n        boolean isLeashed();\n    }\n\n    /** The context. */\n    private final Context ctx;\n\n    /**\n     * Instantiates a new Conformity monkey.\n     *\n     * @param ctx\n     *            the context.\n     */\n    public ConformityMonkey(Context ctx) {\n        super(ctx);\n        this.ctx = ctx;\n    }\n\n    /**\n     * The monkey Type.\n     */\n    public enum Type implements MonkeyType {\n        /** Conformity monkey. */\n        CONFORMITY\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public final Type type() {\n        return Type.CONFORMITY;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Context context() {\n        return ctx;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public abstract void doMonkeyBusiness();\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/ConformityRule.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.conformity;\n\n/**\n * Interface for a conformity check rule.\n */\npublic interface ConformityRule {\n\n    /**\n     * Performs the conformity check against the rule.\n     * @param cluster\n     *          the cluster to check for conformity\n     * @return\n     *          the conformity check result\n     */\n    Conformity check(Cluster cluster);\n\n    /**\n     * Gets the name/id of the rule.\n     * @return\n     *      the name of the rule\n     */\n    String getName();\n\n    /**\n     * Gets the human-readable reason to explain why the cluster is not conforming.\n     * @return the human-readable reason to explain why the cluster is not conforming\n     */\n    String getNonconformingReason();\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/conformity/ConformityRuleEngine.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.conformity;\n\nimport com.google.common.collect.Lists;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.Collections;\n\n/**\n * The class implementing the conformity rule engine.\n */\npublic class ConformityRuleEngine {\n    private static final Logger LOGGER = LoggerFactory.getLogger(ConformityRuleEngine.class);\n\n    private final Collection<ConformityRule> rules = Lists.newArrayList();\n\n    /**\n     * Checks whether a cluster is conforming or not against the rules in the engine. This\n     * method runs the checks the cluster against all the rules.\n     *\n     * @param cluster\n     *            the cluster\n     * @return true if the cluster is conforming, false otherwise.\n     */\n    public boolean check(Cluster cluster) {\n        Validate.notNull(cluster);\n        cluster.clearConformities();\n        for (ConformityRule rule : rules) {\n            if (!cluster.getExcludedRules().contains(rule.getName())) {\n                LOGGER.info(String.format(\"Running conformity rule %s on cluster %s\",\n                        rule.getName(), cluster.getName()));\n                cluster.updateConformity(rule.check(cluster));\n            } else {\n                LOGGER.info(String.format(\"Conformity rule %s is excluded on cluster %s\",\n                        rule.getName(), cluster.getName()));\n            }\n        }\n        boolean isConforming = true;\n        for (Conformity conformity : cluster.getConformties()) {\n            if (!conformity.getFailedComponents().isEmpty()) {\n                isConforming = false;\n            }\n        }\n        cluster.setConforming(isConforming);\n        return isConforming;\n    }\n\n    /**\n     * Add a conformity rule.\n     *\n     * @param rule\n     *            The conformity rule to add.\n     * @return The Conformity rule engine object.\n     */\n    public ConformityRuleEngine addRule(ConformityRule rule) {\n        Validate.notNull(rule);\n        rules.add(rule);\n        return this;\n    }\n\n    /**\n     * Gets all conformity rules in the rule engine.\n     * @return all conformity rules in the rule engine\n     */\n    public Collection<ConformityRule> rules() {\n        return Collections.unmodifiableCollection(rules);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/AbstractJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.janitor;\n\nimport java.util.*;\nimport com.google.common.collect.Maps;\nimport com.netflix.servo.annotations.DataSourceType;\nimport com.netflix.servo.annotations.Monitor;\nimport com.netflix.servo.annotations.MonitorTags;\nimport com.netflix.servo.monitor.BasicCounter;\nimport com.netflix.servo.monitor.Counter;\nimport com.netflix.servo.monitor.MonitorConfig;\nimport com.netflix.servo.monitor.Monitors;\nimport com.netflix.servo.tag.BasicTagList;\nimport com.netflix.servo.tag.TagList;\nimport com.netflix.simianarmy.*;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.Resource.CleanupState;\nimport com.netflix.simianarmy.janitor.JanitorMonkey.EventTypes;\nimport com.netflix.simianarmy.janitor.JanitorMonkey.Type;\nimport org.apache.commons.lang.Validate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * An abstract implementation of Janitor. It marks resources that the rule engine considers\n * invalid as cleanup candidate and sets the expected termination date. It also removes the\n * cleanup candidate flag from resources that no longer exist or the rule engine no longer\n * considers invalid due to change of conditions. For resources marked as cleanup candidates\n * and the expected termination date is passed, the janitor removes the resources from the\n * cloud.\n */\npublic abstract class AbstractJanitor implements Janitor, DryRunnableJanitor {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJanitor.class);\n\n    /** Tags to attach to servo metrics */\n    @MonitorTags\n    protected TagList tags;\n\n    private final String region;\n    /** The region the janitor is running in. */\n    public String getRegion() {\n        return region;\n    }\n\n    /**\n     * The rule engine used to decide if a resource should be a cleanup\n     * candidate.\n     */\n    private final JanitorRuleEngine ruleEngine;\n\n    /** The janitor crawler to get resources from the cloud. */\n    private final JanitorCrawler crawler;\n\n    /** The resource type that the janitor is responsible for to clean up. **/\n    private final ResourceType resourceType;\n\n    /** The janitor resource tracker that is responsible for keeping track of\n     * resource status.\n     */\n    private final JanitorResourceTracker resourceTracker;\n\n    private final Collection<Resource> markedResources = new ArrayList<Resource>();\n\n    private final Collection<Resource> cleanedResources = new ArrayList<Resource>();\n\n    private final Collection<Resource> unmarkedResources = new ArrayList<Resource>();\n\n    private final Collection<Resource> failedToCleanResources = new ArrayList<Resource>();\n\n    private final Collection<Resource> skippedVanishedOrValidResources = new ArrayList<>();\n\n    private final MonkeyCalendar calendar;\n\n    private final MonkeyConfiguration config;\n\n    /** Flag to indicate whether the Janitor is leashed. */\n    private boolean leashed;\n\n    private final MonkeyRecorder recorder;\n\n    /** The number of resources that have been checked on this run. */\n    private int checkedResourcesCount;\n\n    private Counter cleanupDryRunFailureCount = new BasicCounter(MonitorConfig.builder(\"dryRunCleanupFailures\").build());\n\n    /**\n     * Sets the flag to indicate if the janitor is leashed.\n     *\n     * @param isLeashed true if the the janitor is leased, false otherwise.\n     */\n    protected void setLeashed(boolean isLeashed) {\n        this.leashed = isLeashed;\n    }\n\n    /**\n     * The Interface Context.\n     */\n    public interface Context {\n\n        /** Region.\n         *\n         * @return the region\n         */\n        String region();\n\n        /**\n         * Configuration.\n         *\n         * @return the monkey configuration\n         */\n        MonkeyConfiguration configuration();\n\n        /**\n         * Calendar.\n         *\n         * @return the monkey calendar\n         */\n        MonkeyCalendar calendar();\n\n        /**\n         * Janitor rule engine.\n         * @return the janitor rule engine\n         */\n        JanitorRuleEngine janitorRuleEngine();\n\n        /**\n         * Janitor crawler.\n         *\n         * @return the chaos crawler\n         */\n        JanitorCrawler janitorCrawler();\n\n        /**\n         * Janitor resource tracker.\n         *\n         * @return the janitor resource tracker\n         */\n        JanitorResourceTracker janitorResourceTracker();\n\n        /**\n         * Recorder.\n         *\n         * @return the recorder to record events\n         */\n        MonkeyRecorder recorder();\n    }\n\n    /**\n     * Constructor.\n     * @param ctx the context\n     * @param resourceType the resource type the janitor is taking care\n     */\n    public AbstractJanitor(Context ctx, ResourceType resourceType) {\n        Validate.notNull(ctx);\n        Validate.notNull(resourceType);\n        this.region = ctx.region();\n        Validate.notNull(region);\n        this.ruleEngine = ctx.janitorRuleEngine();\n        Validate.notNull(ruleEngine);\n        this.crawler = ctx.janitorCrawler();\n        Validate.notNull(crawler);\n        this.resourceTracker = ctx.janitorResourceTracker();\n        Validate.notNull(resourceTracker);\n        this.calendar = ctx.calendar();\n        Validate.notNull(calendar);\n        this.config = ctx.configuration();\n        Validate.notNull(config);\n        // By default the janitor is leashed.\n        this.leashed = config.getBoolOrElse(\"simianarmy.janitor.leashed\", true);\n        this.resourceType = resourceType;\n        Validate.notNull(resourceType);\n        // recorder could be null and no events are recorded when it is.\n        this.recorder = ctx.recorder();\n\n        // setup servo tags, currently just tag each published metric with the region\n        this.tags = BasicTagList.of(\"simianarmy.janitor.region\", ctx.region());\n\n        // register this janitor with servo\n        String monitorObjName = String.format(\"simianarmy.janitor.%s.%s\", this.resourceType.name(), this.region);\n        Monitors.registerObject(monitorObjName, this);\n    }\n\n    @Override\n    public ResourceType getResourceType() {\n        return resourceType;\n    }\n\n    /**\n     * Clears this object's internal resource lists in preparation for a new\n     * run.\n     *\n     * This is an optional method as regular Janitor processing will\n     * automatically clear resource lists as it runs.\n     *\n     * This method offers an explicit clear so that the resources will be\n     * consistent across the run.  For example, when starting a run after a\n     * previous run has finished, cleanedResources will be holding the cleaned\n     * resources from the prior run until cleanupResources() is called.  By\n     * calling prepareToRun() first, the resource lists will be consistent\n     * for the entire run.\n     */\n    public void prepareToRun() {\n        markedResources.clear();\n        unmarkedResources.clear();\n        checkedResourcesCount = 0;\n        cleanedResources.clear();\n        failedToCleanResources.clear();\n    }\n\n    /**\n     * Marks all resources obtained from the crawler as cleanup candidate if\n     * the janitor rule engine thinks so.\n     */\n    @Override\n    public void markResources() {\n        if (config.getBoolOrElse(\"simianarmy.janitor.skipMark\", false)) {\n            LOGGER.info(\"*****SKIPPING MARKING {}****\", resourceType);\n            return ;\n        }\n\n        markedResources.clear();\n        unmarkedResources.clear();\n        checkedResourcesCount = 0;\n        Map<String, Resource> trackedMarkedResources = getTrackedMarkedResources();\n\n        List<Resource> crawledResources = crawler.resources(resourceType);\n        LOGGER.info(\"Looking for cleanup candidate in {} crawled resources. LeashMode={}\", crawledResources.size(), leashed);\n        Date now = calendar.now().getTime();\n        for (Resource resource : crawledResources) {\n        \tcheckedResourcesCount++;\n            Resource trackedResource = trackedMarkedResources.get(resource.getId());\n            if (!ruleEngine.isValid(resource)) {\n                // If the resource is already marked, ignore it\n                if (trackedResource != null) {\n                    LOGGER.debug(\"Resource {} is already marked. LeashMode={}\", resource.getId(), leashed);\n                    continue;\n                }\n                LOGGER.info(\"Marking resource {} of type {} with expected termination time as {} LeashMode={}\"\n                        , resource.getId(), resource.getResourceType(), resource.getExpectedTerminationTime(), leashed);\n                resource.setState(CleanupState.MARKED);\n                resource.setMarkTime(now);\n                resourceTracker.addOrUpdate(resource);\n                if (!leashed && recorder != null) {\n                    Event evt = recorder.newEvent(Type.JANITOR, EventTypes.MARK_RESOURCE, resource, resource.getId());\n                    recorder.recordEvent(evt);\n                }\n\n                postMark(resource);\n                markedResources.add(resource);\n            } else if (trackedResource != null) {\n                // The resource was marked and now the rule engine does not consider it as a cleanup candidate.\n                // So the janitor needs to unmark the resource.\n                LOGGER.info(\"Unmarking resource {} LeashMode={}\", resource.getId(), leashed);\n                resource.setState(CleanupState.UNMARKED);\n                resourceTracker.addOrUpdate(resource);\n                if (!leashed && recorder != null) {\n                    Event evt = recorder.newEvent(\n                            Type.JANITOR, EventTypes.UNMARK_RESOURCE, resource, resource.getId());\n                    recorder.recordEvent(evt);\n                }\n\n                unmarkedResources.add(resource);\n            }\n        }\n\n        // Unmark the resources that are terminated by user so not returned by the crawler.\n        unmarkUserTerminatedResources(crawledResources, trackedMarkedResources);\n    }\n\n\n    /**\n     * Gets the existing resources that are marked as cleanup candidate. Allowing the subclass to override for e.g.\n     * to handle multi-region.\n     * @return the map from resource id to marked resource\n     */\n    protected Map<String, Resource> getTrackedMarkedResources() {\n        Map<String, Resource> trackedMarkedResources = Maps.newHashMap();\n        for (Resource resource : resourceTracker.getResources(resourceType, Resource.CleanupState.MARKED, region)) {\n            trackedMarkedResources.put(resource.getId(), resource);\n        }\n        return trackedMarkedResources;\n    }\n\n    /**\n     * Cleans up all cleanup candidates that are OK to remove.\n     */\n    @Override\n    public void cleanupResources() {\n        cleanedResources.clear();\n        failedToCleanResources.clear();\n        skippedVanishedOrValidResources.clear();\n        Map<String, Resource> trackedMarkedResources = getTrackedMarkedResources();\n\n        LOGGER.info(\"Checking {} marked resources for cleanup. LeashMode={}\", trackedMarkedResources.size(), leashed);\n        Date now = calendar.now().getTime();\n        for (Resource markedResource : trackedMarkedResources.values()) {\n            if (config.getBoolOrElse(\"simianarmy.janitor.skipVanishedOrValidResources\", false)) {\n                // find matching crawled resource. This ensures we always have the freshest resource.\n                List<Resource> matchingCrawledResources = Optional.ofNullable(crawler.resources(markedResource.getId()))\n                        .orElse(Collections.emptyList());\n\n                LOGGER.info(\"Rechecking resource {} before deletion {} - matching candidates {}\",\n                        markedResource, markedResource.getResourceType(), matchingCrawledResources);\n                Optional<Resource> crawledResource = matchingCrawledResources.stream()\n                        .filter(r -> r.equals(markedResource))\n                        .findFirst();\n\n                if (!crawledResource.isPresent() || ruleEngine.isValid(crawledResource.get())) {\n                    skippedVanishedOrValidResources.add(markedResource);\n                    LOGGER.warn(\"Skipping resource {} that either no longer exists or is now valid\", markedResource);\n                    continue;\n                }\n            }\n\n\n            if (canClean(markedResource, now)) {\n                LOGGER.info(\"Cleaning up resource {} of type {}. LeashMode={}\",\n                        markedResource.getId(), markedResource.getResourceType().name(), leashed);\n                try {\n                    if (leashed) {\n                        cleanupDryRun(markedResource.cloneResource());\n                    } else {\n                        cleanup(markedResource);\n                        markedResource.setActualTerminationTime(now);\n                        markedResource.setState(Resource.CleanupState.JANITOR_TERMINATED);\n                        resourceTracker.addOrUpdate(markedResource);\n                        if (recorder != null) {\n                            Event evt = recorder.newEvent(Type.JANITOR, EventTypes.CLEANUP_RESOURCE, markedResource,\n                                    markedResource.getId());\n                            recorder.recordEvent(evt);\n                        }\n\n                        postCleanup(markedResource);\n                        cleanedResources.add(markedResource);\n                    }\n                } catch (Exception e) {\n                    String message;\n                    if (e instanceof DryRunnableJanitorException) {\n                        message = String.format(\"Failed Dry Run cleanup of resource %s of type %s. LeashMode=%b\",\n                                markedResource.getId(), markedResource.getResourceType().name(), leashed);\n                        cleanupDryRunFailureCount.increment();\n                    } else {\n                        message = String.format(\"Failed to clean up the resource %s of type %s. LeashMode=%b\",\n                                markedResource.getId(), markedResource.getResourceType().name(), leashed);\n                        failedToCleanResources.add(markedResource);\n                    }\n\n                    LOGGER.error(message, e);\n                }\n            }\n        }\n    }\n\n    /** Determines if the input resource can be cleaned. The Janitor calls this method\n     * before cleaning up a resource and only cleans the resource when the method returns\n     * true. A resource is considered to be OK to clean if\n     * 1) it is marked as cleanup candidates\n     * 2) the expected termination time is already passed\n     * 3) the owner has already been notified about the cleanup\n     * 4) the resource is not opted out of Janitor monkey\n     * The method can be overridden in subclasses.\n     * @param resource the resource the Janitor considers to clean\n     * @param now the time that represents the current time\n     * @return true if the resource is OK to clean, false otherwise\n     */\n    protected boolean canClean(Resource resource, Date now) {\n        return resource.getState() == Resource.CleanupState.MARKED\n                && !resource.isOptOutOfJanitor()\n                && resource.getExpectedTerminationTime() != null\n                && resource.getExpectedTerminationTime().before(now)\n                && resource.getNotificationTime() != null\n                && resource.getNotificationTime().before(now);\n    }\n\n    /**\n     * Implements required operations after a resource is marked.\n     * @param resource The resource that is marked\n     */\n    protected abstract void postMark(Resource resource);\n\n    /**\n     * Cleans a resource up, e.g. deleting the resource from the cloud.\n     * @param resource The resource that is cleaned up.\n     */\n    protected abstract void cleanup(Resource resource);\n\n    /**\n     * Implements required operations after a resource is cleaned.\n     * @param resource The resource that is cleaned up.\n     */\n    protected abstract void postCleanup(Resource resource);\n\n    /** gets the resources marked in the last run of the Janitor. */\n    public Collection<Resource> getMarkedResources() {\n        return Collections.unmodifiableCollection(markedResources);\n    }\n\n    /** gets the resources unmarked in the last run of the Janitor. */\n    public Collection<Resource> getUnmarkedResources() {\n        return Collections.unmodifiableCollection(unmarkedResources);\n    }\n\n    /** gets the resources cleaned in the last run of the Janitor. */\n    public Collection<Resource> getCleanedResources() {\n        return Collections.unmodifiableCollection(cleanedResources);\n    }\n\n    /** gets the resources that failed to be cleaned in the last run of the Janitor. */\n    public Collection<Resource> getFailedToCleanResources() {\n        return Collections.unmodifiableCollection(failedToCleanResources);\n    }\n\n    private void unmarkUserTerminatedResources(List<Resource> crawledResources, Map<String, Resource> trackedMarkedResources) {\n        Set<String> crawledResourceIds = new HashSet<String>();\n        for (Resource crawledResource : crawledResources) {\n            crawledResourceIds.add(crawledResource.getId());\n        }\n\n        if (config.getBoolOrElse(\"simianarmy.janitor.unmarkResourceNotReturnedByCrawler\", false)) {\n            for (Resource markedResource : trackedMarkedResources.values()) {\n                if (!crawledResourceIds.contains(markedResource.getId())) {\n                    // The resource does not exist anymore.\n                    LOGGER.info(\"Resource {} is not returned by the crawler. It should already be terminated. LeashMode={}\",\n                            markedResource.getId(), leashed);\n\n                    markedResource.setState(Resource.CleanupState.USER_TERMINATED);\n                    resourceTracker.addOrUpdate(markedResource);\n                    unmarkedResources.add(markedResource);\n                }\n            }\n        }\n    }\n\n    @Monitor(name=\"cleanedResourcesCount\", type=DataSourceType.GAUGE)\n    public int getResourcesCleanedCount() {\n      return cleanedResources.size();\n    }\n\n    @Monitor(name=\"markedResourcesCount\", type=DataSourceType.GAUGE)\n    public int getMarkedResourcesCount() {\n      return markedResources.size();\n    }\n\n    @Monitor(name=\"failedToCleanResourcesCount\", type=DataSourceType.GAUGE)\n    public int getFailedToCleanResourcesCount() {\n      return failedToCleanResources.size();\n    }\n\n    @Monitor(name=\"unmarkedResourcesCount\", type=DataSourceType.GAUGE)\n    public int getUnmarkedResourcesCount() {\n      return unmarkedResources.size();\n    }\n\n    @Monitor(name=\"checkedResourcesCount\", type=DataSourceType.GAUGE)\n    public int getCheckedResourcesCount() {\n      return checkedResourcesCount;\n    }\n\n    @Monitor(name=\"skippedVanishedOrValidResources\", type = DataSourceType.GAUGE)\n    public int skippedVanishedOrValidResources() {\n        return skippedVanishedOrValidResources.size();\n    }\n\n    public Counter getCleanupDryRunFailureCount() {\n        return cleanupDryRunFailureCount;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/DryRunnableJanitor.java",
    "content": "/*\n *\n *  Copyright 2017 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.janitor;\n\nimport com.netflix.simianarmy.Resource;\n\npublic interface DryRunnableJanitor extends Janitor {\n    default void cleanupDryRun(Resource markedResource) throws DryRunnableJanitorException {\n        // NO-OP\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/DryRunnableJanitorException.java",
    "content": "/*\n *\n *  Copyright 2017 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.janitor;\n\npublic class DryRunnableJanitorException extends Exception {\n    public DryRunnableJanitorException(String message) {\n        super(message);\n    }\n\n    public DryRunnableJanitorException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/Janitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.janitor;\n\nimport com.netflix.simianarmy.ResourceType;\n\n/**\n * The interface for a janitor that performs the mark and cleanup operations for\n * cloud resources of a resource type.\n */\npublic interface Janitor {\n\n    /**\n     * Gets the resource type the janitor is cleaning up.\n     * @return the resource type the janitor is cleaning up.\n     */\n    ResourceType getResourceType();\n\n    /**\n     * Mark cloud resources as cleanup candidates and remove the marks for resources\n     * that no longer exist or should not be cleanup candidates anymore.\n     */\n    void markResources();\n\n    /**\n     * Clean the resources up that are marked as cleanup candidates when appropriate.\n     */\n    void cleanupResources();\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/JanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.janitor;\n\nimport java.util.EnumSet;\nimport java.util.List;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\n\n/**\n * The crawler for janitor monkey.\n */\npublic interface JanitorCrawler {\n\n    /**\n     * Resource types.\n     *\n     * @return the type of resources this crawler crawls\n     */\n    EnumSet<? extends ResourceType> resourceTypes();\n\n    /**\n     * Resources crawled by this crawler for a specific resource type.\n     *\n     * @param resourceType the resource type\n     * @return the list\n     */\n    List<Resource> resources(ResourceType resourceType);\n\n    /**\n     * Gets the up to date information for a collection of resource ids. When the input argument is null\n     * or empty, the method returns all resources.\n     *\n     * @param resourceIds\n     *          the resource ids\n     * @return the list of resources\n     */\n    List<Resource> resources(String... resourceIds);\n\n    /**\n     * Gets the owner email for a resource to set the ownerEmail field when crawl.\n     * @param resource the resource\n     * @return the owner email of the resource\n     */\n    String getOwnerEmailForResource(Resource resource);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/JanitorEmailBuilder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.janitor;\n\nimport java.util.Collection;\nimport java.util.Map;\n\nimport com.netflix.simianarmy.AbstractEmailBuilder;\nimport com.netflix.simianarmy.Resource;\n\n/** The abstract class for building Janitor monkey email notifications. */\npublic abstract class JanitorEmailBuilder extends AbstractEmailBuilder {\n\n    /**\n     * Sets the map from an owner email to the resources that belong to the owner\n     * and need to send notifications for.\n     * @param emailToResources the map from owner email to the owned resource\n     */\n    public abstract void setEmailToResources(Map<String, Collection<Resource>> emailToResources);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/JanitorEmailNotifier.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.janitor;\n\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.Resource.CleanupState;\nimport com.netflix.simianarmy.aws.AWSEmailNotifier;\nimport org.apache.commons.lang.Validate;\nimport org.apache.commons.lang.StringUtils;\nimport org.joda.time.DateTime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/** The email notifier implemented for Janitor Monkey. */\npublic class JanitorEmailNotifier extends AWSEmailNotifier {\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(JanitorEmailNotifier.class);\n    private static final String UNKNOWN_EMAIL = \"UNKNOWN\";\n    /**\n     * If the scheduled termination date is within 2 hours of notification date + headsup days,\n     * we don't need to extend the termination date.\n     */\n    private static final int HOURS_IN_MARGIN = 2;\n\n    private final String region;\n    private final String defaultEmail;\n    private final List<String> ccEmails;\n    private final JanitorResourceTracker resourceTracker;\n    private final JanitorEmailBuilder emailBuilder;\n    private final MonkeyCalendar calendar;\n    private final int daysBeforeTermination;\n    private final String sourceEmail;\n    private final String ownerEmailDomain;\n    private final Map<String, Collection<Resource>> invalidEmailToResources =\n            new HashMap<String, Collection<Resource>>();\n\n    /**\n     * The Interface Context.\n     */\n    public interface Context {\n        /**\n         * Gets the Amazon Simple Email Service client.\n         * @return the Amazon Simple Email Service client\n         */\n        AmazonSimpleEmailServiceClient sesClient();\n\n        /**\n         * Gets the source email the notifier uses to send email.\n         * @return the source email\n         */\n        String sourceEmail();\n\n        /**\n         * Gets the default email the notifier sends to when there is no owner specified for a resource.\n         * @return the default email\n         */\n        String defaultEmail();\n\n        /**\n         * Gets the number of days a notification is sent before the expected termination date..\n         * @return the number of days a notification is sent before the expected termination date.\n         */\n        int daysBeforeTermination();\n\n        /**\n         * Gets the region the notifier is running in.\n         * @return the region the notifier is running in.\n         */\n        String region();\n\n        /** Gets the janitor resource tracker.\n         * @return the janitor resource tracker\n         */\n        JanitorResourceTracker resourceTracker();\n\n        /** Gets the janitor email builder.\n         * @return the janitor email builder\n         */\n        JanitorEmailBuilder emailBuilder();\n\n        /** Gets the calendar.\n         * @return the calendar\n         */\n        MonkeyCalendar calendar();\n\n        /** Gets the cc email addresses.\n         * @return the cc email addresses\n         */\n        String[] ccEmails();\n\n        /** Get the default domain of email addresses.\n         * @return the default domain of email addresses\n         */\n        String ownerEmailDomain();\n    }\n\n    /**\n     * Constructor.\n     * @param ctx the context.\n     */\n    public JanitorEmailNotifier(Context ctx) {\n        super(ctx.sesClient());\n        this.region = ctx.region();\n        this.defaultEmail = ctx.defaultEmail();\n        this.daysBeforeTermination = ctx.daysBeforeTermination();\n        this.resourceTracker = ctx.resourceTracker();\n        this.emailBuilder = ctx.emailBuilder();\n        this.calendar = ctx.calendar();\n        this.ccEmails = new ArrayList<String>();\n        String[] ctxCCs = ctx.ccEmails();\n        if (ctxCCs != null) {\n            for (String ccEmail : ctxCCs) {\n                this.ccEmails.add(ccEmail);\n            }\n        }\n        this.sourceEmail = ctx.sourceEmail();\n        this.ownerEmailDomain = ctx.ownerEmailDomain();\n    }\n\n    /**\n     * Gets all the resources that are marked and no notifications have been sent. Send email notifications\n     * for these resources. If there is a valid email address in the ownerEmail field of the resource, send\n     * to that address. Otherwise send to the default email address.\n     */\n    public void sendNotifications() {\n        validateEmails();\n        Map<String, Collection<Resource>> emailToResources = new HashMap<String, Collection<Resource>>();\n        invalidEmailToResources.clear();\n        for (Resource r : getMarkedResources()) {\n            if (r.isOptOutOfJanitor()) {\n                LOGGER.info(String.format(\"Resource %s is opted out of Janitor Monkey so no notification is sent.\",\n                        r.getId()));\n                continue;\n            }\n            if (canNotify(r)) {\n                String email = r.getOwnerEmail();\n                if (email != null && !email.contains(\"@\")\n                      && StringUtils.isNotBlank(this.ownerEmailDomain)) {\n                    email = String.format(\"%s@%s\", email, this.ownerEmailDomain);\n                }\n                if (!isValidEmail(email)) {\n                    if (defaultEmail != null) {\n                        LOGGER.info(String.format(\"Email %s is not valid, send to the default email address %s\",\n                                email, defaultEmail));\n                        putEmailAndResource(emailToResources, defaultEmail, r);\n                    } else {\n                        if (email == null) {\n                            email = UNKNOWN_EMAIL;\n                        }\n                        LOGGER.info(String.format(\"Email %s is not valid and default email is not set for resource %s\",\n                                email, r.getId()));\n                        putEmailAndResource(invalidEmailToResources, email, r);\n                    }\n                } else {\n                    putEmailAndResource(emailToResources, email, r);\n                }\n            } else {\n                LOGGER.debug(String.format(\"Not the time to send notification for resource %s\", r.getId()));\n            }\n        }\n        emailBuilder.setEmailToResources(emailToResources);\n        Date now = calendar.now().getTime();\n        for (Map.Entry<String, Collection<Resource>> entry : emailToResources.entrySet()) {\n            String email = entry.getKey();\n            String emailBody = emailBuilder.buildEmailBody(email);\n            String subject = buildEmailSubject(email);\n            sendEmail(email, subject, emailBody);\n            for (Resource r : entry.getValue()) {\n                LOGGER.debug(String.format(\"Notification is sent for resource %s\", r.getId()));\n                r.setNotificationTime(now);\n                resourceTracker.addOrUpdate(r);\n            }\n            LOGGER.info(String.format(\"Email notification has been sent to %s for %d resources.\",\n                    email, entry.getValue().size()));\n        }\n    }\n\n    /**\n     * Gets the marked resources for notification. Allow overriding in subclasses.\n     * @return the marked resources\n     */\n    protected Collection<Resource> getMarkedResources() {\n        return resourceTracker.getResources(null, CleanupState.MARKED, region);\n    }\n\n    private void validateEmails() {\n        if (defaultEmail != null) {\n            Validate.isTrue(isValidEmail(defaultEmail), String.format(\"Default email %s is invalid\", defaultEmail));\n        }\n        if (ccEmails != null) {\n            for (String ccEmail : ccEmails) {\n                Validate.isTrue(isValidEmail(ccEmail), String.format(\"CC email %s is invalid\", ccEmail));\n            }\n        }\n    }\n\n    @Override\n    public String buildEmailSubject(String email) {\n        return String.format(\"Janitor Monkey Notification for %s\", email);\n    }\n\n    /**\n     * Decides if it is time for sending notification for the resource. This method can be\n     * overridden in subclasses so notifications can be send earlier or later.\n     * @param resource the resource\n     * @return true if it is OK to send notification now, otherwise false.\n     */\n    protected boolean canNotify(Resource resource) {\n        Validate.notNull(resource);\n        if (resource.getState() != CleanupState.MARKED || resource.isOptOutOfJanitor()) {\n            return false;\n        }\n\n        Date notificationTime = resource.getNotificationTime();\n        // We don't want to send notification too early (since things may change) or too late (we need\n        // to give owners enough time to take actions.\n        Date windowStart = new Date(new DateTime(\n                calendar.getBusinessDay(calendar.now().getTime(), daysBeforeTermination).getTime())\n                .minusHours(HOURS_IN_MARGIN).getMillis());\n        Date windowEnd = calendar.getBusinessDay(calendar.now().getTime(), daysBeforeTermination + 1);\n        Date terminationDate = resource.getExpectedTerminationTime();\n        if (notificationTime == null\n                || notificationTime.getTime() == 0\n                || resource.getMarkTime().after(notificationTime)) { // remarked after a notification\n            if (!terminationDate.before(windowStart) && !terminationDate.after(windowEnd)) {\n                // The expected termination time is close enough for sending notification\n                return true;\n            } else if (terminationDate.before(windowStart)) {\n                // The expected termination date is too close. To give the owner time to take possible actions,\n                // we extend the expected termination time here.\n                LOGGER.info(String.format(\"It is less than %d days before the expected termination date,\"\n                        + \" of resource %s, extending the termination time to %s.\",\n                        daysBeforeTermination, resource.getId(), windowStart));\n                resource.setExpectedTerminationTime(windowStart);\n                resourceTracker.addOrUpdate(resource);\n                return true;\n            } else {\n                return false;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Gets the map from invalid email address to the resources that were supposed to be sent to the address.\n     *\n     * @return the map from invalid address to resources that failed to be delivered\n     */\n    public Map<String, Collection<Resource>> getInvalidEmailToResources() {\n        return Collections.unmodifiableMap(invalidEmailToResources);\n    }\n\n    @Override\n    public String[] getCcAddresses(String to) {\n        return ccEmails.toArray(new String[ccEmails.size()]);\n    }\n\n    @Override\n    public String getSourceAddress(String to) {\n        return sourceEmail;\n    }\n\n    private void putEmailAndResource(\n            Map<String, Collection<Resource>> map, String email, Resource resource) {\n        Collection<Resource> resources = map.get(email);\n        if (resources == null) {\n            resources = new ArrayList<Resource>();\n            map.put(email, resources);\n        }\n        resources.add(resource);\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/JanitorMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.janitor;\n\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.MonkeyType;\n\nimport java.util.List;\n\n/**\n * The abstract class for a Janitor Monkey.\n */\npublic abstract class JanitorMonkey extends Monkey {\n\n    /**  The key name of the Janitor tag used to tag resources. */\n    public static final String JANITOR_TAG = \"janitor\";\n    /** The key name of the Janitor meta tag used to tag resources. */\n    public static final String JANITOR_META_TAG = \"JANITOR_META\";\n    /** The key name of the tag instance used to tag resources. */\n    public static final String INSTANCE_TAG_KEY = \"instance\";\n    /** The key name of the tag detach time used to tag resources. */\n    public static final String DETACH_TIME_TAG_KEY = \"detachTime\";\n\n    /**\n     * The Interface Context.\n     */\n    public interface Context extends Monkey.Context {\n\n        /**\n         * Configuration.\n         *\n         * @return the monkey configuration\n         */\n        MonkeyConfiguration configuration();\n\n        /**\n         * Janitors run by this monkey.\n         * @return the janitors\n         */\n        List<AbstractJanitor> janitors();\n\n        /**\n         * Email notifier used to send notifications by the janitor monkey.\n         * @return the email notifier\n         */\n        JanitorEmailNotifier emailNotifier();\n\n        /**\n         * The region the monkey is running in.\n         * @return the region the monkey is running in.\n         */\n        String region();\n\n        /**\n         * The accountName the monkey is running in.\n         * @return the accountName the monkey is running in.\n         */\n        String accountName();\n\n        /**\n         * The Janitor resource tracker.\n         * @return the Janitor resource tracker.\n         */\n        JanitorResourceTracker resourceTracker();\n    }\n\n    /** The context. */\n    private final Context ctx;\n\n    /**\n     * Instantiates a new janitor monkey.\n     *\n     * @param ctx\n     *            the context.\n     */\n    public JanitorMonkey(Context ctx) {\n        super(ctx);\n        this.ctx = ctx;\n    }\n\n    /**\n     * The monkey Type.\n     */\n    public static enum Type implements MonkeyType {\n        /** janitor monkey. */\n        JANITOR\n    }\n\n    /**\n     * The event types that this monkey causes.\n     */\n    public enum EventTypes implements EventType {\n        /** Marking a resource as a cleanup candidate. */\n        MARK_RESOURCE,\n        /** Un-Marking a resource. */\n        UNMARK_RESOURCE,\n        /** Clean up a resource. */\n        CLEANUP_RESOURCE,\n        /** Opt in a resource. */\n        OPT_IN_RESOURCE,\n        /** Opt out a resource. */\n        OPT_OUT_RESOURCE\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public final Type type() {\n        return Type.JANITOR;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public Context context() {\n        return ctx;\n    }\n\n    /** {@inheritDoc} */\n    @Override\n    public abstract void doMonkeyBusiness();\n\n    /**\n     * Opt in a resource for Janitor Monkey.\n     * @param resourceId the resource id\n     * @return the opt-in event\n     */\n    public abstract Event optInResource(String resourceId);\n\n    /**\n     * Opt out a resource for Janitor Monkey.\n     * @param resourceId the resource id\n     * @return the opt-out event\n     */\n    public abstract Event optOutResource(String resourceId);\n\n    /**\n     * Opt in a resource for Janitor Monkey.\n     * @param resourceId the resource id\n     * @param region the region of the resource\n     * @return the opt-in event\n     */\n    public abstract Event optInResource(String resourceId, String region);\n\n    /**\n     * Opt out a resource for Janitor Monkey.\n     * @param resourceId the resource id\n     * @param region the region of the resource\n     * @return the opt-out event\n     */\n    public abstract Event optOutResource(String resourceId, String region);\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/JanitorResourceTracker.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.janitor;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.ResourceType;\n\nimport java.util.List;\n\n/**\n * The interface to track the resources marked/cleaned by the Janitor Monkey.\n *\n */\npublic interface JanitorResourceTracker {\n\n    /**\n     * Adds a resource to the tracker. If the resource with the same id already exists\n     * in the tracker, the method updates the record with the resource parameter.\n     * @param resource the resource to add or update\n     */\n    void addOrUpdate(Resource resource);\n\n    /** Gets the list of resources of a specific resource type and cleanup state in a region.\n     *\n     * @param resourceType the resource type\n     * @param state the cleanup state of the resources\n     * @param region the region of the resources, when the parameter is null, the method returns\n     * resources from all regions\n     * @return list of resources that match the resource type, state and region\n     */\n    List<Resource> getResources(ResourceType resourceType, Resource.CleanupState state, String region);\n\n    /** Gets the resource of a specific id.\n     *\n     * @param resourceId the resource id\n     * @return the resource that matches the resource id\n     */\n    Resource getResource(String resourceId);\n\n    /** Gets the resource of a specific id.\n     *\n     * @param resourceId the resource id\n     * @param regionId the region id\n     * @return the resource that matches the resource id and region\n     */\n    Resource getResource(String resourceId, String regionId);\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/JanitorRuleEngine.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.janitor;\n\nimport com.netflix.simianarmy.Resource;\nimport java.util.List;\n\n/**\n * The interface for janitor rule engine that can decide if a resource should be a candidate of cleanup\n * based on a collection of rules.\n */\npublic interface JanitorRuleEngine {\n\n    /**\n     * Decides whether the resource should be a candidate of cleanup based on the underlying rules.\n     *\n     * @param resource\n     *            The resource\n     * @return true if the resource is valid and should not be a candidate of cleanup based on the underlying rules,\n     *         false otherwise.\n     */\n    boolean isValid(Resource resource);\n\n    /**\n     * Add a rule to decide if a resource should be a candidate for cleanup.\n     *\n     * @param rule\n     *            The rule to decide if a resource should be a candidate for cleanup.\n     * @return The JanitorRuleEngine object.\n     */\n    JanitorRuleEngine addRule(Rule rule);\n\n    /**\n     * Add a rule to decide if a resource should be excluded for cleanup.\n     * Exclusion rules are evaluated before regular rules.  If a resource\n     * matches an exclusion rule, it is excluded from all other cleanup rules.\n     *\n     * @param rule\n     *            The rule to decide if a resource should be excluded for cleanup.\n     * @return The JanitorRuleEngine object.\n     */\n    JanitorRuleEngine addExclusionRule(Rule rule);\n\n    /**\n     * Get rules to find out what's planned for enforcement.\n     *\n     * @return An ArrayList of Rules.\n     */\n    List<Rule> getRules();\n\n    /**\n     * Get rules to find out what's excluded for enforcement.\n     *\n     * @return An ArrayList of Rules.\n     */\n    List<Rule> getExclusionRules();\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/janitor/Rule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n\npackage com.netflix.simianarmy.janitor;\n\nimport com.netflix.simianarmy.Resource;\n\n/**\n * The rule implementing a logic to decide if a resource should be considered as a candidate of cleanup.\n */\npublic interface Rule {\n    /**\n     * Decides whether the resource should be a candidate of cleanup based on the underlying rule. When\n     * the rule considers the resource as a candidate of cleanup, it sets the expected termination time\n     * and termination reason of the resource.\n     *\n     * @param resource\n     *            The resource\n     * @return true if the resource is valid and is not for cleanup, false otherwise\n     */\n    boolean isValid(Resource resource);\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/resources/chaos/ChaosMonkeyResource.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.resources.chaos;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.ws.rs.GET;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.Context;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.UriInfo;\n\nimport com.google.common.base.Strings;\nimport com.netflix.simianarmy.Monkey;\nimport com.sun.jersey.spi.resource.Singleton;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.codehaus.jackson.JsonEncoding;\nimport org.codehaus.jackson.JsonGenerator;\nimport org.codehaus.jackson.JsonNode;\nimport org.codehaus.jackson.map.MappingJsonFactory;\nimport org.codehaus.jackson.map.ObjectMapper;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.netflix.simianarmy.FeatureNotEnabledException;\nimport com.netflix.simianarmy.InstanceGroupNotFoundException;\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.MonkeyRunner;\nimport com.netflix.simianarmy.NotFoundException;\nimport com.netflix.simianarmy.chaos.ChaosMonkey;\nimport com.netflix.simianarmy.chaos.ChaosType;\nimport com.netflix.simianarmy.chaos.ShutdownInstanceChaosType;\n\n/**\n * The Class ChaosMonkeyResource for json REST apis.\n */\n@Path(\"/v1/chaos\")\n@Produces(MediaType.APPLICATION_JSON)\n@Singleton\npublic class ChaosMonkeyResource {\n\n    /** The Constant JSON_FACTORY. */\n    private static final MappingJsonFactory JSON_FACTORY = new MappingJsonFactory();\n\n    /** The monkey. */\n    private ChaosMonkey monkey = null;\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(ChaosMonkeyResource.class);\n\n    /**\n     * Instantiates a chaos monkey resource with a specific chaos monkey.\n     *\n     * @param monkey\n     *          the chaos monkey\n     */\n    public ChaosMonkeyResource(ChaosMonkey monkey) {\n        this.monkey = monkey;\n    }\n\n    /**\n     * Instantiates a chaos monkey resource using a registered chaos monkey from factory.\n     */\n    public ChaosMonkeyResource() {\n        for (Monkey runningMonkey : MonkeyRunner.getInstance().getMonkeys()) {\n            if (runningMonkey instanceof ChaosMonkey) {\n                this.monkey = (ChaosMonkey) runningMonkey;\n                break;\n            }\n        }\n        if (this.monkey == null) {\n            LOGGER.info(\"Creating a new Chaos monkey instance for the resource.\");\n            this.monkey = MonkeyRunner.getInstance().factory(ChaosMonkey.class);\n        }\n    }\n\n    /**\n     * Gets the chaos events. Creates GET /api/v1/chaos api which outputs the chaos events in json. Users can specify\n     * cgi query params to filter the results and use \"since\" query param to set the start of a timerange. \"since\" should\n     * be specified in milliseconds since the epoch.\n     *\n     * @param uriInfo\n     *            the uri info\n     * @return the chaos events json response\n     * @throws IOException\n     *             Signals that an I/O exception has occurred.\n     */\n    @GET\n    public Response getChaosEvents(@Context UriInfo uriInfo) throws IOException {\n        Map<String, String> query = new HashMap<String, String>();\n        Date date = null;\n        for (Map.Entry<String, List<String>> pair : uriInfo.getQueryParameters().entrySet()) {\n            if (pair.getValue().isEmpty()) {\n                continue;\n            }\n            if (pair.getKey().equals(\"since\")) {\n                date = new Date(Long.parseLong(pair.getValue().get(0)));\n            } else {\n                query.put(pair.getKey(), pair.getValue().get(0));\n            }\n        }\n        // if \"since\" not set, default to 24 hours ago\n        if (date == null) {\n            Calendar now = monkey.context().calendar().now();\n            now.add(Calendar.DAY_OF_YEAR, -1);\n            date = now.getTime();\n        }\n\n        List<Event> evts = monkey.context().recorder()\n                .findEvents(ChaosMonkey.Type.CHAOS, ChaosMonkey.EventTypes.CHAOS_TERMINATION, query, date);\n\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos, JsonEncoding.UTF8);\n        gen.writeStartArray();\n        for (Event evt : evts) {\n            gen.writeStartObject();\n            gen.writeStringField(\"monkeyType\", evt.monkeyType().name());\n            gen.writeStringField(\"eventId\", evt.id());\n            gen.writeStringField(\"eventType\", evt.eventType().name());\n            gen.writeNumberField(\"eventTime\", evt.eventTime().getTime());\n            gen.writeStringField(\"region\", evt.region());\n            for (Map.Entry<String, String> pair : evt.fields().entrySet()) {\n                gen.writeStringField(pair.getKey(), pair.getValue());\n            }\n            gen.writeEndObject();\n        }\n        gen.writeEndArray();\n        gen.close();\n        return Response.status(Response.Status.OK).entity(baos.toString(\"UTF-8\")).build();\n    }\n\n    /**\n     * POST /api/v1/chaos will try a add a new event with the information in the url context,\n     * ignoring the monkey probability and max termination configurations, for a specific instance group.\n     *\n     * @param content\n     *            the Json content passed to the http POST request\n     * @return the response\n     * @throws IOException\n     */\n    @POST\n    public Response addEvent(String content) throws IOException {\n        ObjectMapper mapper = new ObjectMapper();\n        LOGGER.info(String.format(\"JSON content: '%s'\", content));\n        JsonNode input = mapper.readTree(content);\n\n        String eventType = getStringField(input, \"eventType\");\n        String groupType = getStringField(input, \"groupType\");\n        String groupName = getStringField(input, \"groupName\");\n        String chaosTypeName = getStringField(input, \"chaosType\");\n\n        ChaosType chaosType;\n        if (!Strings.isNullOrEmpty(chaosTypeName)) {\n            chaosType = ChaosType.parse(this.monkey.getChaosTypes(), chaosTypeName);\n        } else {\n            chaosType = new ShutdownInstanceChaosType(monkey.context().configuration());\n        }\n\n        Response.Status responseStatus;\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos, JsonEncoding.UTF8);\n        gen.writeStartObject();\n        gen.writeStringField(\"eventType\", eventType);\n        gen.writeStringField(\"groupType\", groupType);\n        gen.writeStringField(\"groupName\", groupName);\n        gen.writeStringField(\"chaosType\", chaosType.getKey());\n\n        if (StringUtils.isEmpty(eventType) || StringUtils.isEmpty(groupType) || StringUtils.isEmpty(groupName)) {\n            responseStatus = Response.Status.BAD_REQUEST;\n            gen.writeStringField(\"message\", \"eventType, groupType, and groupName parameters are all required\");\n        } else {\n            if (eventType.equals(\"CHAOS_TERMINATION\")) {\n                responseStatus = addTerminationEvent(groupType, groupName, chaosType, gen);\n            } else {\n                responseStatus = Response.Status.BAD_REQUEST;\n                gen.writeStringField(\"message\", String.format(\"Unrecognized event type: %s\", eventType));\n            }\n        }\n        gen.writeEndObject();\n        gen.close();\n        LOGGER.info(\"entity content is '{}'\", baos.toString(\"UTF-8\"));\n        return Response.status(responseStatus).entity(baos.toString(\"UTF-8\")).build();\n    }\n\n    private Response.Status addTerminationEvent(String groupType,\n            String groupName, ChaosType chaosType, JsonGenerator gen)\n            throws IOException {\n        LOGGER.info(\"Running on-demand termination for instance group type '{}' and name '{}'\",\n                groupType, groupName);\n        Response.Status responseStatus;\n        try {\n            Event evt = monkey.terminateNow(groupType, groupName, chaosType);\n            if (evt != null) {\n                responseStatus = Response.Status.OK;\n                gen.writeStringField(\"monkeyType\", evt.monkeyType().name());\n                gen.writeStringField(\"eventId\", evt.id());\n                gen.writeNumberField(\"eventTime\", evt.eventTime().getTime());\n                gen.writeStringField(\"region\", evt.region());\n                for (Map.Entry<String, String> pair : evt.fields().entrySet()) {\n                    gen.writeStringField(pair.getKey(), pair.getValue());\n                }\n            } else {\n                responseStatus = Response.Status.INTERNAL_SERVER_ERROR;\n                gen.writeStringField(\"message\",\n                        String.format(\"Failed to terminate instance in group %s [type %s]\",\n                                groupName, groupType));\n            }\n        } catch (FeatureNotEnabledException e) {\n            responseStatus = Response.Status.FORBIDDEN;\n            gen.writeStringField(\"message\", e.getMessage());\n        } catch (InstanceGroupNotFoundException e) {\n            responseStatus = Response.Status.NOT_FOUND;\n            gen.writeStringField(\"message\", e.getMessage());\n        } catch (NotFoundException e) {\n            // Available instance cannot be found to terminate, maybe the instance is already gone\n            responseStatus = Response.Status.GONE;\n            gen.writeStringField(\"message\", e.getMessage());\n        }\n        LOGGER.info(\"On-demand termination completed.\");\n        return responseStatus;\n    }\n\n    private String getStringField(JsonNode input, String field) {\n        JsonNode node = input.get(field);\n        if (node == null) {\n            return null;\n        }\n        return node.getTextValue();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/resources/janitor/JanitorMonkeyResource.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.resources.janitor;\n\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.MonkeyRunner;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\nimport org.apache.commons.lang.StringUtils;\nimport org.codehaus.jackson.JsonEncoding;\nimport org.codehaus.jackson.JsonGenerator;\nimport org.codehaus.jackson.JsonNode;\nimport org.codehaus.jackson.map.MappingJsonFactory;\nimport org.codehaus.jackson.map.ObjectMapper;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.ws.rs.GET;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.QueryParam;\nimport javax.ws.rs.core.Context;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.UriInfo;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.util.Map;\n\n/**\n * The Class JanitorMonkeyResource for json REST apis.\n */\n@Path(\"/v1/janitor\")\npublic class JanitorMonkeyResource {\n\n    /** The Constant JSON_FACTORY. */\n    private static final MappingJsonFactory JSON_FACTORY = new MappingJsonFactory();\n\n    /** The monkey. */\n    private static JanitorMonkey monkey;\n\n    /** The Constant LOGGER. */\n    private static final Logger LOGGER = LoggerFactory.getLogger(JanitorMonkeyResource.class);\n\n    /**\n     * Instantiates a janitor monkey resource with a specific janitor monkey.\n     *\n     * @param monkey\n     *          the janitor monkey\n     */\n    public JanitorMonkeyResource(JanitorMonkey monkey) {\n        JanitorMonkeyResource.monkey = monkey;\n    }\n\n    public JanitorMonkeyResource() {\n    }\n\n    public JanitorMonkey getJanitorMonkey() {\n        if (JanitorMonkeyResource.monkey == null ) {\n            JanitorMonkeyResource.monkey = MonkeyRunner.getInstance().factory(JanitorMonkey.class);\n        }\n        return monkey;\n    }\n\n    /**\n     * GET /api/v1/janitor/addEvent will try to a add a new event with the information in the url query string.\n     * This is the same as the regular POST addEvent except through a query string. This technically isn't\n     * very REST-ful as it is a GET call that creates an Opt-out/in event, but is a convenience method\n     * for exposing opt-in/opt-out functionality more directly, for example in an email notification.\n     *\n     * @param eventType eventType from the query string\n     * @param resourceId resourceId from the query string\n     * @return the response\n     * @throws IOException\n     */\n    @GET @Path(\"addEvent\")\n    public Response addEventThroughHttpGet( @QueryParam(\"eventType\") String eventType,  @QueryParam(\"resourceId\") String resourceId,  @QueryParam(\"region\") String region) throws IOException {\n        Response.Status responseStatus;\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        baos.write(\"<html><body style=\\\"text-align:center\\\">\".getBytes());\n        if (StringUtils.isEmpty(eventType) || StringUtils.isEmpty(resourceId)) {\n            responseStatus = Response.Status.BAD_REQUEST;\n            baos.write(\"<p>NOPE!<br/><br/>Janitor didn't get that: eventType and resourceId parameters are both required</p>\".getBytes());\n        } else {\n            ByteArrayOutputStream baos2 = new ByteArrayOutputStream();\n            JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos2, JsonEncoding.UTF8);\n            gen.writeStartObject();\n            gen.writeStringField(\"eventType\", eventType);\n            gen.writeStringField(\"resourceId\", resourceId);\n\n        \tif (eventType.equals(\"OPTIN\")) {\n                responseStatus = optInResource(resourceId, true, region, gen);\n            } else if (eventType.equals(\"OPTOUT\")) {\n                responseStatus = optInResource(resourceId, false, region, gen);\n            } else {\n                responseStatus = Response.Status.BAD_REQUEST;\n                gen.writeStringField(\"message\", String.format(\"Unrecognized event type: %s\", eventType));\n            }\n            gen.writeEndObject();\n            gen.close();\n\n        \tif(responseStatus == Response.Status.OK) {\n        \t\tbaos.write((\"<p>SUCCESS!<br/><br/>Resource <strong>\" + resourceId + \"</strong> has been \" + eventType + \" of Janitor Monkey!</p>\").getBytes());\n        \t} else {\n        \t\tbaos.write((\"<p>NOPE!<br/><br/>Janitor is Confused! Error processing Resource <strong>\" + resourceId + \"</strong></p>\").getBytes());\n        \t}\n\n        \tString jsonout = String.format(\"<p><em>Monkey JSON Response:</em><br/><br/><textarea cols=40 rows=20>%s</textarea></p>\", baos2.toString());\n   \t    \tbaos.write(jsonout.getBytes());\n\n        }\n    \tbaos.write(\"</body></html>\".getBytes());\n        return Response.status(responseStatus).entity(baos.toString(\"UTF-8\")).build();\n    }\n\n    /**\n     * POST /api/v1/janitor will try a add a new event with the information in the url context.\n     *\n     * @param content\n     *            the Json content passed to the http POST request\n     * @return the response\n     * @throws IOException\n     */\n    @POST\n    public Response addEvent(String content) throws IOException {\n        ObjectMapper mapper = new ObjectMapper();\n        LOGGER.info(String.format(\"JSON content: '%s'\", content));\n        JsonNode input = mapper.readTree(content);\n\n        String eventType = getStringField(input, \"eventType\");\n        String resourceId = getStringField(input, \"resourceId\");\n        String region = getStringField(input, \"region\");\n\n        Response.Status responseStatus;\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos, JsonEncoding.UTF8);\n        gen.writeStartObject();\n        gen.writeStringField(\"eventType\", eventType);\n        gen.writeStringField(\"resourceId\", resourceId);\n\n        if (StringUtils.isEmpty(eventType) || StringUtils.isEmpty(resourceId)) {\n            responseStatus = Response.Status.BAD_REQUEST;\n            gen.writeStringField(\"message\", \"eventType and resourceId parameters are all required\");\n        } else {\n            if (eventType.equals(\"OPTIN\")) {\n                responseStatus = optInResource(resourceId, true, region, gen);\n            } else if (eventType.equals(\"OPTOUT\")) {\n                responseStatus = optInResource(resourceId, false, region, gen);\n            } else {\n                responseStatus = Response.Status.BAD_REQUEST;\n                gen.writeStringField(\"message\", String.format(\"Unrecognized event type: %s\", eventType));\n            }\n        }\n        gen.writeEndObject();\n        gen.close();\n        LOGGER.info(\"entity content is '{}'\", baos.toString(\"UTF-8\"));\n        return Response.status(responseStatus).entity(baos.toString(\"UTF-8\")).build();\n    }\n\n    /**\n     * Gets the janitor status (e.g. to support an AWS ELB Healthcheck on an instance running JanitorMonkey).\n     * Creates GET /api/v1/janitor api which responds 200 OK if JanitorMonkey is running.\n     *\n     * @param uriInfo\n     *            the uri info\n     * @return the chaos events json response\n     * @throws IOException\n     *             Signals that an I/O exception has occurred.\n     */\n    @GET\n    public Response getJanitorStatus(@Context UriInfo uriInfo) throws IOException {\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos, JsonEncoding.UTF8);\n        gen.writeStartArray();\n\tgen.writeStartObject();\n\tgen.writeStringField(\"JanitorMonkeyStatus\", \"OnLikeDonkeyKong\");\n\tgen.writeEndObject();\n        gen.writeEndArray();\n        gen.close();\n        return Response.status(Response.Status.OK).entity(baos.toString(\"UTF-8\")).build();\n    }\n\n    private Response.Status optInResource(String resourceId, boolean optIn, String region, JsonGenerator gen)\n            throws IOException {\n        String op = optIn ? \"in\" : \"out\";\n        LOGGER.info(String.format(\"Opt %s resource %s for Janitor Monkey.\", op, resourceId));\n        Response.Status responseStatus;\n        Event evt;\n        if (optIn) {\n            evt = getJanitorMonkey().optInResource(resourceId, region);\n        } else {\n            evt = getJanitorMonkey().optOutResource(resourceId, region);\n        }\n        if (evt != null) {\n            responseStatus = Response.Status.OK;\n            gen.writeStringField(\"monkeyType\", evt.monkeyType().name());\n            gen.writeStringField(\"eventId\", evt.id());\n            gen.writeNumberField(\"eventTime\", evt.eventTime().getTime());\n            gen.writeStringField(\"region\", evt.region());\n            for (Map.Entry<String, String> pair : evt.fields().entrySet()) {\n                gen.writeStringField(pair.getKey(), pair.getValue());\n            }\n        } else {\n            responseStatus = Response.Status.INTERNAL_SERVER_ERROR;\n            gen.writeStringField(\"message\",\n                    String.format(\"Failed to opt %s resource %s\", op, resourceId));\n        }\n        LOGGER.info(String.format(\"Opt %s operation completed.\", op));\n        return responseStatus;\n    }\n\n    private String getStringField(JsonNode input, String field) {\n        JsonNode node = input.get(field);\n        if (node == null) {\n            return null;\n        }\n        return node.getTextValue();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/tunable/TunableInstanceGroup.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.tunable;\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.netflix.simianarmy.GroupType;\nimport com.netflix.simianarmy.basic.chaos.BasicInstanceGroup;\n\nimport java.util.List;\n\n/**\n * Allows for individual InstanceGroups to alter the aggressiveness\n * of ChaosMonkey.\n * \n * @author jeffggardner\n *\n */\npublic class TunableInstanceGroup extends BasicInstanceGroup {\n  \n  public TunableInstanceGroup(String name, GroupType type, String region, List<TagDescription> tags) {\n    super(name, type, region, tags);\n  }\n\n  private double aggressionCoefficient = 1.0;\n\n  /**\n   * @return the aggressionCoefficient\n   */\n  public final double getAggressionCoefficient() {\n    return aggressionCoefficient;\n  }\n\n  /**\n   * @param aggressionCoefficient the aggressionCoefficient to set\n   */\n  public final void setAggressionCoefficient(double aggressionCoefficient) {\n    this.aggressionCoefficient = aggressionCoefficient;\n  }\n\n  \n}\n"
  },
  {
    "path": "src/main/java/com/netflix/simianarmy/tunable/TunablyAggressiveChaosMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.tunable;\n\nimport com.netflix.simianarmy.basic.chaos.BasicChaosMonkey;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\n\n/**\n * This class modifies the probability by multiplying the configured\n * probability by the aggression coefficient tag on the instance group.\n * \n * @author jeffggardner\n */\npublic class TunablyAggressiveChaosMonkey extends BasicChaosMonkey {\n\n  public TunablyAggressiveChaosMonkey(Context ctx) {\n    super(ctx);\n  }\n\n  /**\n   * Gets the tuned probability value, returns 0 if the group is not\n   * enabled. Calls getEffectiveProbability and modifies that value if \n   * the instance group is a TunableInstanceGroup.\n   * \n   * @param group The instance group\n   * @return the effective probability value for the instance group\n   */\n  @Override\n  protected double getEffectiveProbability(InstanceGroup group) {\n\n    if (!isGroupEnabled(group)) {\n      return 0;\n    }\n\n    double probability = getEffectiveProbabilityFromCfg(group);\n    \n    // if this instance group is tunable, then factor in the aggression coefficient\n    if (group instanceof TunableInstanceGroup ) {\n      TunableInstanceGroup tunable = (TunableInstanceGroup) group;\n      probability *= tunable.getAggressionCoefficient();\n    }\n    \n    return probability; \n  }\n}\n"
  },
  {
    "path": "src/main/resources/chaos.properties",
    "content": "# The file contains the properties for Chaos Monkey.\n# see documentation at:\n# https://github.com/Netflix/SimianArmy/wiki/Configuration\n\n# let chaos run\nsimianarmy.chaos.enabled = true\n\n# don't allow chaos to kill (ie dryrun mode)\nsimianarmy.chaos.leashed = true\n\n# set to \"false\" for Opt-In behavior, \"true\" for Opt-Out behavior\nsimianarmy.chaos.ASG.enabled = false\n\n# uncomment this line to use tunable aggression\n#simianarmy.client.chaos.class = com.netflix.simianarmy.tunable.TunablyAggressiveChaosMonkey\n\n# default probability for all ASGs\nsimianarmy.chaos.ASG.probability = 1.0\n\n# increase or decrease the termination limit\nsimianarmy.chaos.ASG.maxTerminationsPerDay = 1.0\n\n# Strategies\nsimianarmy.chaos.shutdowninstance.enabled = true\nsimianarmy.chaos.blockallnetworktraffic.enabled = false\nsimianarmy.chaos.burncpu.enabled = false\nsimianarmy.chaos.killprocesses.enabled = false\nsimianarmy.chaos.nullroute.enabled = false\nsimianarmy.chaos.failec2.enabled = false\nsimianarmy.chaos.faildns.enabled = false\nsimianarmy.chaos.faildynamodb.enabled = false\nsimianarmy.chaos.fails3.enabled = false\nsimianarmy.chaos.networkcorruption.enabled = false\nsimianarmy.chaos.networklatency.enabled = false\nsimianarmy.chaos.networkloss.enabled = false\n\n# Force-detaching EBS volumes may cause data loss\nsimianarmy.chaos.detachvolumes.enabled = false\n\n# FillDisk fills the root disk.\n# NOTE: This may incur charges for an EBS root volume.  See burnmoney option.\nsimianarmy.chaos.burnio.enabled = false\n# BurnIO causes disk activity on the root disk.\n# NOTE: This may incur charges for an EBS root volume. See burnmoney option.\nsimianarmy.chaos.filldisk.enabled = false\n\n# Where we know the chaos strategy will incur charges, we won't run it unless burnmoney is true.\nsimianarmy.chaos.burnmoney = false\n\n\n# enable a specific ASG\n# simianarmy.chaos.ASG.<asgName>.enabled = true\n# simianarmy.chaos.ASG.<asgName>.probability = 1.0\n\n# increase or decrease the termination limit for a specific ASG\n# simianarmy.chaos.ASG.<asgName>.maxTerminationsPerDay = 1.0\n\n# Enroll in mandatory terminations.  If a group has not had a\n# termination within the windowInDays range then it will terminate\n# one instance in the group with a 0.5 probability (at some point in\n# the next 2 days an instance should be terminated), then\n# do nothing again for windowInDays.  This forces \"enabled\" groups\n# that have a probability of 0.0 to have terminations periodically.\nsimianarmy.chaos.mandatoryTermination.enabled = false\nsimianarmy.chaos.mandatoryTermination.windowInDays = 32\nsimianarmy.chaos.mandatoryTermination.defaultProbability = 0.5\n\n# Enable notification for Chaos termination for a specific instance group\n# simianarmy.chaos.<groupType>.<groupName>.notification.enabled = true\n\n# Set the destination email the termination notification sent to for a specific instance group\n# simianarmy.chaos.<groupType>.<groupName>.ownerEmail = foo@bar.com\n\n# Set the source email that sends the termination notification\n# simianarmy.chaos.notification.sourceEmail = foo@bar.com\n\n# Enable notification for Chaos termination for all instance groups\n#simianarmy.chaos.notification.global.enabled = true\n\n# Set the destination email the termination notification is sent to for all instance groups\n#simianarmy.chaos.notification.global.receiverEmail = foo@bar.com\n\n# Set a prefix applied to the subject of all termination notifications\n# Probably want to include a trailing space to separate from start of default text\n#simianarmy.chaos.notification.subject.prefix = SubjectPrefix \n\n# Set a suffix applied to the subject of all termination notifications\n# Probably want to include an escaped space \" \\ \" to separate from end of default text\n#simianarmy.chaos.notification.subject.suffix =  \\ SubjectSuffix\n\n# Set a prefix applied to the body of all termination notifications\n# Probably want to include a trailing space to separate from start of default text\n#simianarmy.chaos.notification.body.prefix = BodyPrefix \n\n# Set a suffix applied to the body of all termination notifications\n# Probably want to include an escaped space \" \\ \" to separate from end of default text\n#simianarmy.chaos.notification.body.suffix =  \\ BodySuffix\n\n# Enable the email subject to be the same as the body, to include terminated instance and group information\n#simianarmy.chaos.notification.subject.isBody = true\n#set the tag filter on the ASGs to terminate only instances from the ASG with the this tag key and value\n#simianarmy.chaos.ASGtag.key = chaos_monkey\n#simianarmy.chaos.ASGtag.value = true\n"
  },
  {
    "path": "src/main/resources/client.properties",
    "content": "#####################################################################\n### Configure which client and context to use.\n#####################################################################\n\n### The default implementation is to use an AWS Client, equaling a property like the following:\n#\n#simianarmy.client.context.class=com.netflix.simianarmy.basic.BasicContext\n\n### to use an VSphereClient instead, uncomment this:\n#\n#simianarmy.client.context.class=com.netflix.simianarmy.client.vsphere.VSphereContext\n#\n### configure the specific selected client, e.g for VSphere these are\n#\n#simianarmy.client.vsphere.url=https://YOUR_VSPHERE_SERVER/sdk\n#simianarmy.client.vsphere.username=YOUR_SERVICE_ACCOUNT_USERNAME\n#simianarmy.client.vsphere.password=YOUR_SERVICE_ACCOUNT_PASSWORD\n\n### configure the specific selected client, e.g for AWS these are\n\n### both \"accountKey\" and \"secretKey\" can be left blank or be removed,\n### if the credentials are provided as environment variable or\n### an instance role is used to handle permissions\n### see: http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-roles.html\n#simianarmy.client.aws.accountKey = fakeAccount\n#simianarmy.client.aws.secretKey  = fakeSecret\n### Comment out the following line to detect the AWS region where the instance is running\nsimianarmy.client.aws.region = us-west-1\n\n### Common account name to make it easier to identify emails by subject\nsimianarmy.client.aws.accountName = default\n\n### To operate under an assumed role - the role will be assumed for all activity, sts:AssumeRole \n### action must be allowed for the inital IAM role being used (long lived credentials)\n### http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html \n#\n#simianarmy.client.aws.assumeRoleArn = arn:aws:iam::ACCOUNT:role/ROLE\n\n### The VSpehere client uses a TerminationStrategy for killing VirtualMachines\n### You can configure which property and value for it to set prior to resetting the VirtualMachine\n#\n#simianarmy.client.vsphere.terminationStrategy.property.name=Force Boot\n#simianarmy.client.vsphere.terminationStrategy.property.value=server\n\n# Uncomment to use a version of Monkey recorder that does not rely on AWS SDB\n#simianarmy.client.recorder.class=com.netflix.simianarmy.basic.LocalDbRecorder\n\n### Operate in Cloud Formation mode - the random suffix appended to Auto Scaling Group names is ignored\n### (specify ASG names as usual with no suffix in chaos.properties)  \n#\n#simianarmy.client.chaos.class=com.netflix.simianarmy.basic.chaos.CloudFormationChaosMonkey\n\n# Use the following if a proxy is needed to connect to AWS APIs\n# proxyHost and proxyPort are required to connect through a proxy, proxyUser and proxyPassword are optional\n#simianarmy.client.aws.proxyHost=\n#simianarmy.client.aws.proxyPort=\n#simianarmy.client.aws.proxyUser=\n#simianarmy.client.aws.proxyPassword=\n"
  },
  {
    "path": "src/main/resources/conformity.properties",
    "content": "# let Conformity monkey run\nsimianarmy.conformity.enabled = true\n\n# dryrun mode, no email notification to the owner of nonconforming clusters is sent\nsimianarmy.conformity.leashed = true\n\n# By default Conformity Monkey wakes up every hour\nsimianarmy.scheduler.frequency = 1\nsimianarmy.scheduler.frequencyUnit = HOURS\nsimianarmy.scheduler.threads = 1\n\n# Conformity Monkey runs every hour.\nsimianarmy.calendar.openHour = 0\nsimianarmy.calendar.closeHour = 24\nsimianarmy.calendar.timezone = America/Los_Angeles\n\n# override to force monkey time, useful for debugging off hours\n#simianarmy.calendar.isMonkeyTime = true\n\n# Conformity monkey sends notifications to the owner of unconforming clusters between the open hour and close\n# hour only. In other hours, only summary email is sent. The default setting is to always send email notifications\n# after each run.\nsimianarmy.conformity.notification.openHour = 0\nsimianarmy.conformity.notification.closeHour = 24\n\nsimianarmy.conformity.sdb.domain = SIMIAN_ARMY\n\n# The property below needs to be a valid email address to receive the summary email of Conformity Monkey\n# after each run\nsimianarmy.conformity.summaryEmail.to = foo@bar.com\n\n# The property below needs to be a valid email address to send notifications for Conformity monkey\nsimianarmy.conformity.notification.defaultEmail = foo@bar.com\n\n# The property below needs to be a valid email address to send notifications for Conformity Monkey\nsimianarmy.conformity.notification.sourceEmail = foo@bar.com\n\n# By default Eureka is not enabled. The conformity rules that need to access Eureka are not added\n# when Eureka is not enabled.\nsimianarmy.conformity.Eureka.enabled = false\n\n# The following property is used to enable the conformity rule to check whether there is mismatch of availability\n# zones between any auto scaling group and its ELBs in a cluster.\nsimianarmy.conformity.rule.SameZonesInElbAndAsg.enabled = true\n\n# The following property is used to enable the conformity rule to check whether all instances in the cluster\n# are in required security groups.\nsimianarmy.conformity.rule.InstanceInSecurityGroup.enabled = true\n\n# The following property specifies the required security groups in the InstanceInSecurityGroup conformity rule.\nsimianarmy.conformity.rule.InstanceInSecurityGroup.requiredSecurityGroups = nf-infrastructure, nf-datacenter\n\n# The following property is used to enable the conformity rule to check whether there is any instance that is\n# older than certain days.\nsimianarmy.conformity.rule.InstanceTooOld.enabled = true\n\n# The following property specifies the number of days used in the InstanceInSecurityGroup, any instance that is\n# old than this number of days is consider nonconforming.\nsimianarmy.conformity.rule.InstanceTooOld.instanceAgeThreshold = 180\n\n# The following property is used to enable the conformity rule to check whether all instances in the cluster\n# have a status url defined according to Discovery/Eureka.\nsimianarmy.conformity.rule.InstanceHasStatusUrl.enabled = true\n\n# The following property is used to enable the conformity rule to check whether all instances in the cluster\n# have a health check url defined according to Discovery/Eureka.\nsimianarmy.conformity.rule.InstanceHasHealthCheckUrl.enabled = true\n\n# The following property is used to enable the conformity rule to check whether there are unhealthy instances\n# in the cluster accoring to Discovery/Eureka.\nsimianarmy.conformity.rule.InstanceIsHealthyInEureka.enabled = true\n\n# You can override a cluster's owner email by providing a property here. For example, the line below overrides\n# the owner email of cluster foo to foo@bar.com\n# simianarmy.conformity.cluster.foo.ownerEmail = foo@bar.com\n\n# You can exclude specific conformity rules for a cluster using this property. For example, the line below excludes\n# the conformity rule rule1 and rule2 on cluster foo.\n# simianarmy.conformity.cluster.foo.excludedRules = rule1,rule2\n\n# You can opt out a cluster completely from Conformity Monkey by using this property. After a cluster is opted out,\n# no notification is sent for it no matter it is conforming or not. For example, the line below opts out the cluster\n# foo.\n# simianarmy.conformity.cluster.foo.optedOut = true\n\n"
  },
  {
    "path": "src/main/resources/janitor.properties",
    "content": "# see documentation at:\n# https://github.com/Netflix/SimianArmy/wiki/Configuration\n\n# By default Janitor Monkey wakes up every hour\nsimianarmy.scheduler.frequency = 1\nsimianarmy.scheduler.frequencyUnit = HOURS\nsimianarmy.scheduler.threads = 1\n# Janitor Monkey runs every day at 11am.\nsimianarmy.calendar.openHour = 11\nsimianarmy.calendar.closeHour = 11\nsimianarmy.calendar.timezone = America/Los_Angeles\n\n# Let Janitor Monkey run\nsimianarmy.janitor.enabled = true\n\n# Don't allow Janitor Monkey to change resources (dryrun mode)\nsimianarmy.janitor.leashed = true\n\n# The SDB domain for storing the resources managed by the Janitor Monkey.\nsimianarmy.janitor.resources.sdb.domain = SIMIAN_ARMY\n\n# override to force monkey time, useful for debugging off hours\n#simianarmy.calendar.isMonkeyTime = true\n\n# Currently Janitor Monkey can clean up the following resources\nsimianarmy.janitor.enabledResources = Instance, ASG, EBS_Volume, EBS_Snapshot, Launch_Config\n\n# The property below needs to be a valid email address to send notifications for Janitor Monkey\nsimianarmy.janitor.notification.sourceEmail = foo@bar.com\n\n# The property below needs to be a valid email address to receive the summary email of Janitor Monkey\n# after each run\nsimianarmy.janitor.summaryEmail.to = foo@bar.com\n\n# The property below needs to be a valid email address to receive the notifications of Janitor Monkey\n# for resouces that do not have a valid owner email specified\nsimianarmy.janitor.notification.defaultEmail = foo@bar.com\n\n# The property below specifies the number of business days that a notification is sent before the\n# expected termination time. For example, if a resource is scheduled to be cleaned up by Janitor\n# Monkey on 12/13/2012, Thursday and the property is set to 2, the owner will receive notification\n# about the cleanup on 12/11/2012, Tuesday, which is 2 business days before the termination date.\nsimianarmy.janitor.notification.daysBeforeTermination = 2\n\n# The owner id that snapshots have for being managed by Janitor Monkey. This property needs\n# to set if you use Edda for getting snapshots to avoid getting shared snapshots. If you are using\n# AWS to get the snapshots, this property does not need to be set.\n#simianarmy.janitor.snapshots.ownerId = 012345678\n\n# The following properties are used by the Janitor rule for cleaning up orphaned instances,\n# i.e. instances that are not in an auto-scaling group.\nsimianarmy.janitor.rule.orphanedInstanceRule.enabled = true\n# An orphaned instance is marked as cleanup candidate if it has launched for more than the number\n# of days specified in the property below.\nsimianarmy.janitor.rule.orphanedInstanceRule.instanceAgeThreshold = 2\n# The number of business days the instance is kept after a notification is sent for the termination\n# when the instance has an owner.\nsimianarmy.janitor.rule.orphanedInstanceRule.retentionDaysWithOwner = 3\n# The number of business days the instance is kept after a notification is sent for the termination\n# when the instance has no owner.\nsimianarmy.janitor.rule.orphanedInstanceRule.retentionDaysWithoutOwner = 8\n# If true, don't consider members of an OpsWorks stack as orphans\nsimianarmy.janitor.rule.orphanedInstanceRule.opsworks.parentage = false\n\n# The following properties are used by the Janitor rule for cleaning up untagged resources,\n# i.e. instances that are missing any required tags\nsimianarmy.janitor.rule.untaggedRule.enabled = false \n# List of tags that are required for each resource\nsimianarmy.janitor.rule.untaggedRule.requiredTags = owner, purpose, project\n# List of resource types that require tags\nsimianarmy.janitor.rule.untaggedRule.resources = Instance, ASG, EBS_Volume, EBS_Snapshot\n# The number of business days the resource is kept after a notification is sent for the deletion\n# when the resource has an owner.\nsimianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner = 2\n# The number of business days the resource is kept after a notification is sent for the deletion\n# when the resource has no owner.\nsimianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner = 2\n\n# The following properties are used by the Janitor rule for cleaning up volumes that have been\n# detached from instances for certain days.\nsimianarmy.janitor.rule.oldDetachedVolumeRule.enabled = true\n# A volume is considered a cleanup candidate after being detached for the number of days specified\n# in the property below.\nsimianarmy.janitor.rule.oldDetachedVolumeRule.detachDaysThreshold = 30\n# The number of business days the volume is kept after a notification is sent for the termination.\nsimianarmy.janitor.rule.oldDetachedVolumeRule.retentionDays = 7\n\n# The following properties are used by the Janitor rule for cleaning up volumes that should have been\n# deleted by AWS when the attached instance was terminated. This rule can be enabled only if Edda\n# is enabled since Janitor Monkey needs to query the history of the attached instance.\nsimianarmy.janitor.rule.deleteOnTerminationRule.enabled = true\n# The number of business days the volume is kept after a notification is sent for the termination.\nsimianarmy.janitor.rule.deleteOnTerminationRule.retentionDays = 3\n\n# The following properties are used by the Janitor rule for cleaning up snapshots that have no existing\n# images generated from them and launched for certain days.\nsimianarmy.janitor.rule.noGeneratedAMIRule.enabled = true\n# A snapshot without an image is considered a cleanup candidate after launching for the number of\n# days specified in the property below.\nsimianarmy.janitor.rule.noGeneratedAMIRule.ageThreshold = 30\n# The number of business days the snapshot is kept after a notification is sent for the termination.\nsimianarmy.janitor.rule.noGeneratedAMIRule.retentionDays = 7\n\n# The following properties are used by the Janitor rule for cleaning up auto-scaling groups that have\n# no active instances and the launch configuration is older than certain days.\nsimianarmy.janitor.rule.oldEmptyASGRule.enabled = true\n# An an auto-scaling group without active instances is considered a cleanup candidate when its launch\n# configuration is older than the number of days specified in the property below.\nsimianarmy.janitor.rule.oldEmptyASGRule.launchConfigAgeThreshold = 50\n# The number of business days the auto-scaling group is kept after a notification is sent for the termination.\nsimianarmy.janitor.rule.oldEmptyASGRule.retentionDays = 10\n\n# The following properties are used by the Janitor rule for cleaning up auto-scaling groups that have\n# no active instances and have been suspended from the associated ELB traffic for certain days.\nsimianarmy.janitor.rule.suspendedASGRule.enabled = true\n# An auto-scaling group without active instances is considered a cleanup candidate when it has been\n# suspended from the associated ELB traffic for the number of days specified in the property below.\nsimianarmy.janitor.rule.suspendedASGRule.suspensionAgeThreshold = 2\n# The number of business days the auto-scaling group is kept after a notification is sent for the termination.\nsimianarmy.janitor.rule.suspendedASGRule.retentionDays = 5\n\n# The property below specifies whether or not Eureka/Discovery is available for Janitor monkey to use.\n# Discovery/Eureka is used in the rules for cleaning up auto-scaling groups to decide if an auto-scaling group\n# has an 'active' instance, i.e. an instance that is registered and up in Discovery/Eureka.\nsimianarmy.janitor.Eureka.enabled = false\n\n# The following properties are used by the Janitor rule for cleaning up launch configurations that are not\n# used by any auto scaling group and are older than certain days.\nsimianarmy.janitor.rule.oldUnusedLaunchConfigRule.enabled = true\n# An unused launch configuration is considered a cleanup candidate when it is older than the number of days\n# specified in the property below.\nsimianarmy.janitor.rule.oldUnusedLaunchConfigRule.ageThreshold = 4\n# The number of business days the launch configuration is kept after a notification is sent for the termination.\nsimianarmy.janitor.rule.oldUnusedLaunchConfigRule.retentionDays = 3\n\n# The property below specifies the number of days to look back in the history when crawling the last reference\n# information of the images.\nsimianarmy.janitor.image.crawler.lookBackDays = 60\n# The owner id that images have for being managed by Janitor Monkey.\n#simianarmy.janitor.image.ownerId = 1234567890\n\n# The following properties are used by the Janitor rule for cleaning up images that are not\n# used by any instance or launch configuration, and not used to create other images for more than certain days.\n# This rule is by default disabled, you need to have Edda running and enabled for using this rule.\nsimianarmy.janitor.rule.unusedImageRule.enabled = false\n# An unused image is considered a cleanup candidate when it is not referenced for than the number of days\n# specified in the property below.\nsimianarmy.janitor.rule.unusedImageRule.lastReferenceDaysThreshold = 45\n# The number of business days the image is kept after a notification is sent for the termination.\nsimianarmy.janitor.rule.unusedImageRule.retentionDays = 3\n\n# You can enable Edda for Janitor Monkey to get better performance and accuracy. You need to start Edda first\n# to be able to use it.\n# simianarmy.janitor.edda.enabled = true\n# The Edda endpoint in each region you need Janitor Monkey\n#simianarmy.janitor.edda.endpoint.us-east-1 = http://localhost:8080\n\n# The Edda client configurations.\n# simianarmy.janitor.edda.client.timeout = 30000\n# simianarmy.janitor.edda.client.retries = 3\n# simianarmy.janitor.edda.client.retryInterval = 1000\n\n# Append domain if owner tag has no domain\n# simianarmy.janitor.notification.ownerEmailDomain = bar.com\n"
  },
  {
    "path": "src/main/resources/log4j.properties",
    "content": "log4j.rootLogger=INFO, stdout\n\n# stdout\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} - %-5p %C{1} - [%F:%L] %m%n\n"
  },
  {
    "path": "src/main/resources/scripts/burncpu.sh",
    "content": "#!/bin/bash\n# Script for BurnCpu Chaos Monkey\n\ncat << EOF > /tmp/infiniteburn.sh\n#!/bin/bash\nwhile true;\n    do openssl speed;\ndone\nEOF\n\n# 32 parallel 100% CPU tasks should hit even the biggest EC2 instances\nfor i in {1..32}\ndo\n    nohup /bin/bash /tmp/infiniteburn.sh &\ndone"
  },
  {
    "path": "src/main/resources/scripts/burnio.sh",
    "content": "#!/bin/bash\n# Script for BurnIO Chaos Monkey\n\ncat << EOF > /tmp/loopburnio.sh\n#!/bin/bash\nwhile true;\ndo\n    dd if=/dev/urandom of=/burn bs=1M count=1024 iflag=fullblock\ndone\nEOF\n\nnohup /bin/bash /tmp/loopburnio.sh &\n"
  },
  {
    "path": "src/main/resources/scripts/faildns.sh",
    "content": "#!/bin/bash\n# Script for FailDns Chaos Monkey\n\n# Block all traffic on port 53\niptables -A INPUT -p tcp -m tcp --dport 53 -j DROP\niptables -A INPUT -p udp -m udp --dport 53 -j DROP\n"
  },
  {
    "path": "src/main/resources/scripts/faildynamodb.sh",
    "content": "#!/bin/bash\n# Script for FailDynamoDb Chaos Monkey\n\n# Block well-known Amazon DynamoDB API endpoints\necho \"127.0.0.1 dynamodb.us-east-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 dynamodb.us-northeast-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 dynamodb.us-gov-west-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 dynamodb.us-west-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 dynamodb.us-west-2.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 dynamodb.sa-east-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 dynamodb.ap-southeast-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 dynamodb.ap-southeast-2.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 dynamodb.eu-west-1.amazonaws.com\" >> /etc/hosts\n\n\n"
  },
  {
    "path": "src/main/resources/scripts/failec2.sh",
    "content": "#!/bin/bash\n# Script for FailEc2 Chaos Monkey\n\n# Block well-known Amazon EC2 API endpoints\necho \"127.0.0.1 ec2.us-east-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 ec2.us-northeast-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 ec2.us-gov-west-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 ec2.us-west-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 ec2.us-west-2.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 ec2.sa-east-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 ec2.ap-southeast-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 ec2.ap-southeast-2.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 ec2.eu-west-1.amazonaws.com\" >> /etc/hosts\n\n\n"
  },
  {
    "path": "src/main/resources/scripts/fails3.sh",
    "content": "#!/bin/bash\n# Script for FailS3 Chaos Monkey\n\n# See http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region\n\necho \"127.0.0.1 s3.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 s3-external-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 s3-us-west-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 s3-us-west-2.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 s3-eu-west-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 s3-ap-southeast-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 s3-ap-southeast-2.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 s3-ap-northeast-1.amazonaws.com\" >> /etc/hosts\necho \"127.0.0.1 s3-sa-east-1.amazonaws.com\" >> /etc/hosts\n\n"
  },
  {
    "path": "src/main/resources/scripts/filldisk.sh",
    "content": "#!/bin/bash\n# Script for FillDisk Chaos Monkey\n\n# 65 GB should be enough to fill up all EC2 root disks!\nnohup dd if=/dev/urandom of=/burn bs=1M count=65536 iflag=fullblock &\n"
  },
  {
    "path": "src/main/resources/scripts/killprocesses.sh",
    "content": "#!/bin/bash\n# Script for KillProcesses Chaos Monkey\n\ncat << EOF > /tmp/kill_loop.sh\n#!/bin/bash\nwhile true;\ndo\n    pkill -KILL -f java\n    pkill -KILL -f python\n    sleep 1\ndone\nEOF\n\nnohup /bin/bash /tmp/kill_loop.sh &\n"
  },
  {
    "path": "src/main/resources/scripts/networkcorruption.sh",
    "content": "#!/bin/bash\n# Script for NetworkCorruption Chaos Monkey\n\n# Corrupts 5% of packets\nsudo tc qdisc add dev eth0 root netem corrupt 5%\n"
  },
  {
    "path": "src/main/resources/scripts/networklatency.sh",
    "content": "#!/bin/bash\n# Script for NetworkLatency Chaos Monkey\n\n# Adds 1000ms +- 500ms of latency to each packet\nsudo tc qdisc add dev eth0 root netem delay 1000ms 500ms\n\n"
  },
  {
    "path": "src/main/resources/scripts/networkloss.sh",
    "content": "#!/bin/bash\n# Script for NetworkLoss Chaos Monkey\n\n# Drops 7% of packets, with 25% correlation with previous packet loss\n# 7% is high, but it isn't high enough that TCP will fail entirely\nsudo tc qdisc add dev eth0 root netem loss 7% 25%\n\n\n"
  },
  {
    "path": "src/main/resources/scripts/nullroute.sh",
    "content": "#!/bin/bash\n# Script for NullRoute Chaos Monkey\n\nip route add blackhole 10.0.0.0/8\n"
  },
  {
    "path": "src/main/resources/simianarmy.properties",
    "content": "# see documentation at:\n# https://github.com/Netflix/SimianArmy/wiki/Configuration\n\nsimianarmy.recorder.sdb.domain = SIMIAN_ARMY\n\n# If using a non-SimbleDB recorder (LocalDB), these settings tweak defaults.\n# Following should be a writeable location, for monkey events when SimpleDB is not used\n#simianarmy.recorder.localdb.file=/tmp/simianarmy_events\n# Max number of events to record; old events will be expired after this limit is\n# reached.  Use this to avoid filling disk with events (or attach a big volume!)\n#simianarmy.recorder.localdb.max_events=1000000\n# Optional password to encrypt event storage.\n#simianarmy.recorder.localdb.password=some_secret\n\nsimianarmy.scheduler.frequency = 1\nsimianarmy.scheduler.frequencyUnit = HOURS\nsimianarmy.scheduler.threads = 1\nsimianarmy.calendar.openHour = 9\nsimianarmy.calendar.closeHour = 15\nsimianarmy.calendar.timezone = America/Los_Angeles\n# override to force monkey time, useful for debugging off hours\n#simianarmy.calendar.isMonkeyTime = true\n\n# Allows you to Set the (case sensitive) AWS Tag Key to use for owner tags; e.g. Owner or owner\n# Will be Monkey Wide - used by all Monkeys. If not set defaults to \"owner\"\n# simianarmy.tags.owner = Owner\n\n# Region override for Amazon Simple Email Service Client\n#simianarmy.aws.email.region=us-west-1"
  },
  {
    "path": "src/main/resources/volumeTagging.properties",
    "content": "# see documentation at:\n# https://github.com/Netflix/SimianArmy/wiki/Configuration\n\n# The properties in this file are used by the monkey that tags volumes with information that\n# Janitor Monkey will need for cleaning up volumes.\n\n# Let the monkey run.\nsimianarmy.volumeTagging.enabled = true\n# Running in the dryrun mode, no tagging is really done.\nsimianarmy.volumeTagging.leashed = true\n\n# Set the property below if you need the owner alias to be converted to a valid email address\n#simianarmy.volumeTagging.ownerEmailDomain = foo.com\n\n# The volume tagging monkey always runs. The tagging process is needed by the Janitor Monkey to\n# clean up volumes. We can keep the volume tagging monkey running so we don't miss any change\n# of volumes.\nsimianarmy.calendar.isMonkeyTime = true\n"
  },
  {
    "path": "src/main/webapp/WEB-INF/web.xml",
    "content": "<web-app version=\"2.5\" xmlns=\"http://java.sun.com/xml/ns/javaee\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee \">\n    <display-name>Simian Army</display-name>\n    <servlet>\n        <servlet-name>Monkey Server</servlet-name>\n        <servlet-class>com.netflix.simianarmy.basic.BasicMonkeyServer</servlet-class>\n        <load-on-startup>1</load-on-startup>\n    </servlet>\n    <servlet>\n      <servlet-name>jersey-servlet</servlet-name>\n      <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>\n      <init-param>\n        <param-name>com.sun.jersey.config.property.packages</param-name>\n        <param-value>com.netflix.simianarmy.resources</param-value>\n      </init-param>\n      <load-on-startup>2</load-on-startup>\n    </servlet>\n    <servlet-mapping>\n      <servlet-name>jersey-servlet</servlet-name>\n      <url-pattern>/api/*</url-pattern>\n    </servlet-mapping>\n</web-app>\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/TestMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy;\n\nimport org.testng.annotations.Test;\nimport org.testng.Assert;\n\npublic class TestMonkey extends Monkey {\n    public TestMonkey() {\n        super(new TestMonkeyContext(Type.TEST));\n    }\n\n    public enum Type implements MonkeyType {\n        TEST\n    };\n\n    public Type type() {\n        return Type.TEST;\n    }\n\n    public void doMonkeyBusiness() {\n        Assert.assertTrue(true, \"ran monkey business\");\n    }\n\n    @Test\n    public void testStart() {\n        this.start();\n    }\n\n    @Test\n    public void testStop() {\n        this.stop();\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/TestMonkeyContext.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\nimport com.netflix.simianarmy.MonkeyRecorder.Event;\nimport com.netflix.simianarmy.basic.BasicConfiguration;\nimport com.netflix.simianarmy.basic.BasicRecorderEvent;\nimport org.jclouds.compute.ComputeService;\nimport org.jclouds.domain.LoginCredentials;\nimport org.jclouds.ssh.SshClient;\nimport org.testng.Assert;\n\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\npublic class TestMonkeyContext implements Monkey.Context {\n    private final MonkeyType monkeyType;\n    private final LinkedList<Event> eventReport = new LinkedList<Event>();\n\n    public TestMonkeyContext(MonkeyType monkeyType) {\n        this.monkeyType = monkeyType;\n    }\n\n    @Override\n    public MonkeyConfiguration configuration() {\n        return new BasicConfiguration(new Properties());\n    }\n\n    @Override\n    public MonkeyScheduler scheduler() {\n        return new MonkeyScheduler() {\n            @Override\n            public int frequency() {\n                return 1;\n            }\n\n            @Override\n            public TimeUnit frequencyUnit() {\n                return TimeUnit.HOURS;\n            }\n\n            @Override\n            public void start(Monkey monkey, Runnable run) {\n                Assert.assertEquals(monkey.type().name(), monkeyType.name(), \"starting monkey\");\n                run.run();\n            }\n\n            @Override\n            public void stop(Monkey monkey) {\n                Assert.assertEquals(monkey.type().name(), monkeyType.name(), \"stopping monkey\");\n            }\n        };\n    }\n\n    @Override\n    public MonkeyCalendar calendar() {\n        // CHECKSTYLE IGNORE MagicNumberCheck\n        return new MonkeyCalendar() {\n            @Override\n            public boolean isMonkeyTime(Monkey monkey) {\n                return true;\n            }\n\n            @Override\n            public int openHour() {\n                return 10;\n            }\n\n            @Override\n            public int closeHour() {\n                return 11;\n            }\n\n            @Override\n            public Calendar now() {\n                return Calendar.getInstance();\n            }\n\n            @Override\n            public Date getBusinessDay(Date date, int n) {\n                throw new RuntimeException(\"Not implemented.\");\n            }\n        };\n    }\n\n    @Override\n    public CloudClient cloudClient() {\n        return new CloudClient() {\n            @Override\n            public void terminateInstance(String instanceId) {\n            }\n\n            @Override\n            public void createTagsForResources(Map<String, String> keyValueMap, String... resourceIds) {\n            }\n\n            @Override\n            public void deleteAutoScalingGroup(String asgName) {\n            }\n\n            @Override\n            public void deleteVolume(String volumeId) {\n            }\n\n            @Override\n            public void deleteSnapshot(String snapshotId) {\n            }\n\n            @Override\n            public void deleteImage(String imageId) {\n            }\n\n            @Override\n            public void deleteElasticLoadBalancer(String elbId) {\n            }\n\n            @Override\n            public void deleteDNSRecord(String dnsname, String dnstype, String hostedzoneid) {\n            }\n\n            @Override\n            public void deleteLaunchConfiguration(String launchConfigName) {\n            }\n\n            @Override\n            public List<String> listAttachedVolumes(String instanceId, boolean includeRoot) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public void detachVolume(String instanceId, String volumeId,\n                    boolean force) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public ComputeService getJcloudsComputeService() {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public String getJcloudsId(String instanceId) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public SshClient connectSsh(String instanceId, LoginCredentials credentials) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public String findSecurityGroup(String instanceId, String groupName) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public String createSecurityGroup(String instanceId, String groupName, String description) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public boolean canChangeInstanceSecurityGroups(String instanceId) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public void setInstanceSecurityGroups(String instanceId, List<String> groupIds) {\n                throw new UnsupportedOperationException();\n            }\n        };\n    }\n\n    private final MonkeyRecorder recorder = new MonkeyRecorder() {\n        private final List<Event> events = new LinkedList<Event>();\n\n        @Override\n        public Event newEvent(MonkeyType mkType, EventType eventType, String region, String id) {\n            return new BasicRecorderEvent(mkType, eventType, region, id);\n        }\n\n        @Override\n        public void recordEvent(Event evt) {\n            events.add(evt);\n        }\n\n        @Override\n        public List<Event> findEvents(Map<String, String> query, Date after) {\n            return events;\n        }\n\n        @Override\n        public List<Event> findEvents(MonkeyType mkeyType, Map<String, String> query, Date after) {\n            // used from BasicScheduler\n            return events;\n        }\n\n        @Override\n        public List<Event> findEvents(MonkeyType mkeyType, EventType eventType, Map<String, String> query, Date after) {\n            // used from ChaosMonkey\n            List<Event> evts = new LinkedList<Event>();\n            for (Event evt : events) {\n                if (query.get(\"groupName\").equals(evt.field(\"groupName\")) && evt.monkeyType() == mkeyType\n                        && evt.eventType() == eventType && evt.eventTime().after(after)) {\n                    evts.add(evt);\n                }\n            }\n            return evts;\n        }\n    };\n\n    @Override\n    public MonkeyRecorder recorder() {\n        return recorder;\n    }\n\n    @Override\n    public void reportEvent(Event evt) {\n        eventReport.add(evt);\n    }\n\n    @Override\n    public void resetEventReport() {\n        eventReport.clear();\n    }\n\n    @Override\n    public String getEventReport() {\n        StringBuilder report = new StringBuilder();\n        for (Event event : eventReport) {\n            report.append(event.eventType());\n            report.append(\" \");\n            report.append(event.id());\n        }\n        return report.toString();\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/TestMonkeyRunner.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy;\n\nimport java.util.List;\n\nimport org.testng.annotations.Test;\nimport org.testng.Assert;\n\npublic class TestMonkeyRunner {\n    private static boolean monkeyARan = false;\n\n    private static class MonkeyA extends TestMonkey {\n        public void doMonkeyBusiness() {\n            monkeyARan = true;\n        }\n    }\n\n    private static boolean monkeyBRan = false;\n\n    private static class MonkeyB extends Monkey {\n        public enum Type implements MonkeyType {\n            B\n        };\n\n        public Type type() {\n            return Type.B;\n        }\n\n        public interface Context extends Monkey.Context {\n            boolean getTrue();\n        }\n\n        private Context ctx;\n\n        public MonkeyB(Context ctx) {\n            super(ctx);\n            this.ctx = ctx;\n        }\n\n        public void doMonkeyBusiness() {\n            monkeyBRan = ctx.getTrue();\n        }\n    }\n\n    private static class MonkeyBContext extends TestMonkeyContext implements MonkeyB.Context {\n        public MonkeyBContext() {\n            super(MonkeyB.Type.B);\n        }\n\n        public boolean getTrue() {\n            return true;\n        }\n    }\n\n    @Test\n    void testInstance() {\n        Assert.assertEquals(MonkeyRunner.getInstance(), MonkeyRunner.INSTANCE);\n    }\n\n    @Test\n    void testRunner() {\n\n        MonkeyRunner runner = MonkeyRunner.getInstance();\n\n        runner.addMonkey(MonkeyA.class);\n        runner.replaceMonkey(MonkeyA.class);\n\n        runner.addMonkey(MonkeyB.class, MonkeyBContext.class);\n        runner.replaceMonkey(MonkeyB.class, MonkeyBContext.class);\n\n        List<Monkey> monkeys = runner.getMonkeys();\n        Assert.assertEquals(monkeys.size(), 2);\n        Assert.assertEquals(monkeys.get(0).type().name(), \"TEST\");\n        Assert.assertEquals(monkeys.get(1).type().name(), \"B\");\n\n        Monkey a = runner.factory(MonkeyA.class);\n        Assert.assertEquals(a.type().name(), \"TEST\");\n\n        Monkey b = runner.factory(MonkeyB.class, MonkeyBContext.class);\n        Assert.assertEquals(b.type().name(), \"B\");\n\n        Assert.assertNull(runner.getContextClass(MonkeyA.class));\n        Assert.assertEquals(runner.getContextClass(MonkeyB.class), MonkeyBContext.class);\n\n        runner.start();\n\n        Assert.assertEquals(monkeyARan, true, \"monkeyA ran\");\n        Assert.assertEquals(monkeyBRan, true, \"monkeyB ran\");\n\n        runner.stop();\n\n        runner.removeMonkey(MonkeyA.class);\n        Assert.assertEquals(monkeys.size(), 1);\n        Assert.assertEquals(monkeys.get(0).type().name(), \"B\");\n\n        runner.removeMonkey(MonkeyB.class);\n        Assert.assertEquals(monkeys.size(), 0);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/TestUtils.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy;\n\nimport static org.joda.time.DateTimeConstants.MILLIS_PER_DAY;\n\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\n\n/** Utility class for test cases.\n * @author mgeis\n *\n */\npublic final class TestUtils {\n\n    private TestUtils() {\n        //this should never be called\n        //if called internally, throw an error\n        throw new InstantiationError(\"Instantiation of TestUtils utility class prohibited.\");\n    }\n\n    /** Verify that the termination date is roughly retentionDays from now\n     * By 'roughly' we mean within one day.  There are times (twice per year)\n     * when certain tests execute and the Daylight Savings cutover makes it not\n     * a precisely rounded day amount (for example, a termination policy of 4 days\n     * will really be about 3.95 days, or 95 hours, because one hour is lost as\n     * the clocks \"spring ahead\").\n     *\n     * A more precise, but complicated logic could be written to make sure that \"roughly\"\n     * means not more than an hour before and not more than an hour after the anticipated\n     * cutoff, but that makes the test much less readable.\n     *\n     * By just making sure that the difference between the actual and proposed dates\n     * is less than one day, we get a rough idea of whether the termination time was correct.\n     * @param resource The AWS Resource to be checked\n     * @param retentionDays number of days it should be kept around\n     * @param timeOfCheck The time the check is run\n     */\n    public static void verifyTerminationTimeRough(Resource resource, int retentionDays, DateTime timeOfCheck) {\n        long days = (resource.getExpectedTerminationTime().getTime() - timeOfCheck.getMillis()) / MILLIS_PER_DAY;\n        Assert.assertTrue(Math.abs(days - retentionDays) <= 1);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/TestAWSEmailNotifier.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.aws;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\npublic class TestAWSEmailNotifier extends AWSEmailNotifier {\n    public TestAWSEmailNotifier() {\n        super(null);\n    }\n\n    @Override\n    public String buildEmailSubject(String to) {\n        return null;\n    }\n\n    @Override\n    public String[] getCcAddresses(String to) {\n        return new String[0];\n    }\n\n    @Override\n    public String getSourceAddress(String to) {\n        return null;\n    }\n\n    @Test\n    public void testEmailWithHashIsValid() {\n        TestAWSEmailNotifier emailNotifier = new TestAWSEmailNotifier();\n        Assert.assertTrue(emailNotifier.isValidEmail(\"#bla-#name@domain-test.com\"), \"Email with hash is not valid\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/TestRDSRecorder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.aws;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.ArgumentMatcher;\nimport org.mockito.Matchers;\nimport org.mockito.Mockito;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.core.RowMapper;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.MonkeyType;\nimport com.netflix.simianarmy.basic.BasicRecorderEvent;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\npublic class TestRDSRecorder extends RDSRecorder {\n\n    private static final String REGION = \"us-west-1\";\n\t\n    public TestRDSRecorder() {\n    \tsuper(mock(JdbcTemplate.class), \"recordertable\", REGION);\n    }\n\n    public enum Type implements MonkeyType {\n        MONKEY\n    }\n\n    public enum EventTypes implements EventType {\n        EVENT\n    }\n    \n    @Test\n    public void testInit() {        \n        TestRDSRecorder recorder = new TestRDSRecorder();\t\n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        Mockito.doNothing().when(recorder.getJdbcTemplate()).execute(sqlCap.capture());\n        recorder.init();        \n        Assert.assertEquals(sqlCap.getValue(), \"create table if not exists recordertable ( eventId varchar(255), eventTime BIGINT, monkeyType varchar(255), eventType varchar(255), region varchar(255), dataJson varchar(4096) )\");\n    }    \n\n\t@SuppressWarnings(\"unchecked\")\n    @Test\n    public void testInsertNewRecordEvent() {\n    \t// mock the select query that is issued to see if the record already exists\n        ArrayList<Event> events = new ArrayList<>();\n        TestRDSRecorder recorder = new TestRDSRecorder();\n\t\twhen(recorder.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                            Matchers.any(Object[].class), \n         \t\t                            Matchers.any(RowMapper.class))).thenReturn(events);\n\n\t\tEvent evt = newEvent(Type.MONKEY, EventTypes.EVENT, \"region\", \"testId\");\n        evt.addField(\"field1\", \"value1\");\n        evt.addField(\"field2\", \"value2\");\n        \n        // this will be ignored as it conflicts with reserved key\n        evt.addField(\"id\", \"ignoreThis\");\n        \n        ArgumentCaptor<Object> objCap = ArgumentCaptor.forClass(Object.class);\n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        when(recorder.getJdbcTemplate().update(sqlCap.capture(), \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture())).thenReturn(1);\n        recorder.recordEvent(evt);\n        List<Object> args = objCap.getAllValues();        \n        Assert.assertEquals(sqlCap.getValue(), \"insert into recordertable (eventId,eventTime,monkeyType,eventType,region,dataJson) values (?,?,?,?,?,?)\");\n        Assert.assertEquals(args.size(), 6);\n        Assert.assertEquals(args.get(0).toString(), evt.id());\n        Assert.assertEquals(args.get(1).toString(), evt.eventTime().getTime() + \"\");\n        Assert.assertEquals(args.get(2).toString(), SimpleDBRecorder.enumToValue(evt.monkeyType()));\n        Assert.assertEquals(args.get(3).toString(), SimpleDBRecorder.enumToValue(evt.eventType()));\n        Assert.assertEquals(args.get(4).toString(), evt.region());\n    }\n\n    private Event mkSelectResult(String id, Event evt) {\n    \tevt.addField(\"field1\", \"value1\");\n    \tevt.addField(\"field2\", \"value2\");\n        return evt;\n    }\n\n\t@SuppressWarnings(\"unchecked\")\n    @Test\n    public void testFindEvent() {    \t\n        Event evt1 = new BasicRecorderEvent(Type.MONKEY, EventTypes.EVENT, \"region\", \"testId1\", 1330538400000L);\n        mkSelectResult(\"testId1\", evt1);\n        Event evt2 = new BasicRecorderEvent(Type.MONKEY, EventTypes.EVENT, \"region\", \"testId2\", 1330538400000L);\n        mkSelectResult(\"testId2\", evt2);\n        \n        ArrayList<Event> events = new ArrayList<>();\n        TestRDSRecorder recorder = new TestRDSRecorder();\n        events.add(evt1);\n        events.add(evt2);\n\t\twhen(recorder.getJdbcTemplate().query(Matchers.anyString(), \n\t\t\t\tMatchers.argThat(new ArgumentMatcher<Object []>(){\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic boolean matches(Object argument) {\n\t\t\t\t\t\tObject [] args = (Object [])argument;\n\t\t\t\t\t\tAssert.assertTrue(args[0] instanceof String);\n\t\t\t\t\t\tAssert.assertEquals((String)args[0],REGION);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}), \n         \t\tMatchers.any(RowMapper.class))).thenReturn(events);\n        \n        Map<String, String> query = new LinkedHashMap<String, String>();\n        query.put(\"instanceId\", \"testId1\");\n\n        verifyEvents(recorder.findEvents(query, new Date(0)));\n    }\n\n    void verifyEvents(List<Event> events) {\n        Assert.assertEquals(events.size(), 2);\n\n        Assert.assertEquals(events.get(0).id(), \"testId1\");\n        Assert.assertEquals(events.get(0).eventTime().getTime(), 1330538400000L);\n        Assert.assertEquals(events.get(0).monkeyType(), Type.MONKEY);\n        Assert.assertEquals(events.get(0).eventType(), EventTypes.EVENT);\n        Assert.assertEquals(events.get(0).field(\"field1\"), \"value1\");\n        Assert.assertEquals(events.get(0).field(\"field2\"), \"value2\");\n        Assert.assertEquals(events.get(0).fields().size(), 2);\n\n        Assert.assertEquals(events.get(1).id(), \"testId2\");\n        Assert.assertEquals(events.get(1).eventTime().getTime(), 1330538400000L);\n        Assert.assertEquals(events.get(1).monkeyType(), Type.MONKEY);\n        Assert.assertEquals(events.get(1).eventType(), EventTypes.EVENT);\n        Assert.assertEquals(events.get(1).field(\"field1\"), \"value1\");\n        Assert.assertEquals(events.get(1).field(\"field2\"), \"value2\");\n        Assert.assertEquals(events.get(1).fields().size(), 2);\n    }\n\n\t@SuppressWarnings(\"unchecked\")\n    @Test\n    public void testFindEventNotFound() {\n        ArrayList<Event> events = new ArrayList<>();\n        TestRDSRecorder recorder = new TestRDSRecorder();\n\t\twhen(recorder.getJdbcTemplate().query(Matchers.anyString(), \n\t\t\t\tMatchers.argThat(new ArgumentMatcher<Object []>(){\n\t\t\t\t\t@Override\n\t\t\t\t\tpublic boolean matches(Object argument) {\n\t\t\t\t\t\tObject [] args = (Object [])argument;\n\t\t\t\t\t\tAssert.assertTrue(args[0] instanceof String);\n\t\t\t\t\t\tAssert.assertEquals((String)args[0],REGION);\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}), \n         \t\tMatchers.any(RowMapper.class))).thenReturn(events);\n\t\t\n\t\tList<Event> results = recorder.findEvents(new HashMap<String, String>(), new Date());\n\t\tAssert.assertEquals(results.size(), 0);\n    }\n    \n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/TestSimpleDBRecorder.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.aws;\n\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Arrays;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.mockito.ArgumentCaptor;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.amazonaws.services.simpledb.AmazonSimpleDB;\nimport com.amazonaws.services.simpledb.model.Attribute;\nimport com.amazonaws.services.simpledb.model.Item;\nimport com.amazonaws.services.simpledb.model.PutAttributesRequest;\nimport com.amazonaws.services.simpledb.model.ReplaceableAttribute;\nimport com.amazonaws.services.simpledb.model.SelectRequest;\nimport com.amazonaws.services.simpledb.model.SelectResult;\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.MonkeyType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\npublic class TestSimpleDBRecorder extends SimpleDBRecorder {\n\n    private static AWSClient makeMockAWSClient() {\n        AmazonSimpleDB sdbMock = mock(AmazonSimpleDB.class);\n        AWSClient awsClient = mock(AWSClient.class);\n        when(awsClient.sdbClient()).thenReturn(sdbMock);\n        when(awsClient.region()).thenReturn(\"region\");\n        return awsClient;\n    }\n\n    public TestSimpleDBRecorder() {\n        super(makeMockAWSClient(), \"DOMAIN\");\n        sdbMock = super.sdbClient();\n    }\n\n    private final AmazonSimpleDB sdbMock;\n\n    @Override\n    protected AmazonSimpleDB sdbClient() {\n        return sdbMock;\n    }\n\n    protected AmazonSimpleDB superSdbClient() {\n        return super.sdbClient();\n    }\n\n    @Test\n    public void testClients() {\n        TestSimpleDBRecorder recorder1 = new TestSimpleDBRecorder();\n        Assert.assertNotNull(recorder1.superSdbClient(), \"non null super sdbClient\");\n\n        TestSimpleDBRecorder recorder2 = new TestSimpleDBRecorder();\n        Assert.assertNotNull(recorder2.superSdbClient(), \"non null super sdbClient\");\n    }\n\n    public enum Type implements MonkeyType {\n        MONKEY\n    }\n\n    public enum EventTypes implements EventType {\n        EVENT\n    }\n\n    @Test\n    public void testRecordEvent() {\n        ArgumentCaptor<PutAttributesRequest> arg = ArgumentCaptor.forClass(PutAttributesRequest.class);\n\n        Event evt = newEvent(Type.MONKEY, EventTypes.EVENT, \"region\", \"testId\");\n        evt.addField(\"field1\", \"value1\");\n        evt.addField(\"field2\", \"value2\");\n        // this will be ignored as it conflicts with reserved key\n        evt.addField(\"id\", \"ignoreThis\");\n\n        recordEvent(evt);\n\n        verify(sdbMock).putAttributes(arg.capture());\n\n        PutAttributesRequest req = arg.getValue();\n        Assert.assertEquals(req.getDomainName(), \"DOMAIN\");\n        Assert.assertEquals(req.getItemName(), \"MONKEY-testId-region-\" + evt.eventTime().getTime());\n        Map<String, String> map = new HashMap<String, String>();\n        for (ReplaceableAttribute attr : req.getAttributes()) {\n            map.put(attr.getName(), attr.getValue());\n        }\n\n        Assert.assertEquals(map.remove(\"id\"), \"testId\");\n        Assert.assertEquals(map.remove(\"eventTime\"), String.valueOf(evt.eventTime().getTime()));\n        Assert.assertEquals(map.remove(\"region\"), \"region\");\n        Assert.assertEquals(map.remove(\"recordType\"), \"MonkeyEvent\");\n        Assert.assertEquals(map.remove(\"monkeyType\"), \"MONKEY|com.netflix.simianarmy.aws.TestSimpleDBRecorder$Type\");\n        Assert.assertEquals(map.remove(\"eventType\"),\n            \"EVENT|com.netflix.simianarmy.aws.TestSimpleDBRecorder$EventTypes\");\n        Assert.assertEquals(map.remove(\"field1\"), \"value1\");\n        Assert.assertEquals(map.remove(\"field2\"), \"value2\");\n        Assert.assertEquals(map.size(), 0);\n    }\n\n    private SelectResult mkSelectResult(String id) {\n        Item item = new Item();\n        List<Attribute> attrs = new LinkedList<Attribute>();\n        attrs.add(new Attribute(\"id\", id));\n        attrs.add(new Attribute(\"eventTime\", \"1330538400000\"));\n        attrs.add(new Attribute(\"region\", \"region\"));\n        attrs.add(new Attribute(\"recordType\", \"MonkeyEvent\"));\n        attrs.add(new Attribute(\"monkeyType\", \"MONKEY|com.netflix.simianarmy.aws.TestSimpleDBRecorder$Type\"));\n        attrs.add(new Attribute(\"eventType\", \"EVENT|com.netflix.simianarmy.aws.TestSimpleDBRecorder$EventTypes\"));\n        attrs.add(new Attribute(\"field1\", \"value1\"));\n        attrs.add(new Attribute(\"field2\", \"value2\"));\n        item.setAttributes(attrs);\n        item.setName(\"MONKEY-\" + id + \"-region\");\n        SelectResult result = new SelectResult();\n        result.setItems(Arrays.asList(item));\n        return result;\n    }\n\n    @Test\n    public void testFindEvent() {\n        SelectResult result1 = mkSelectResult(\"testId1\");\n        result1.setNextToken(\"nextToken\");\n        SelectResult result2 = mkSelectResult(\"testId2\");\n\n        ArgumentCaptor<SelectRequest> arg = ArgumentCaptor.forClass(SelectRequest.class);\n\n        when(sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2);\n\n        Map<String, String> query = new LinkedHashMap<String, String>();\n        query.put(\"instanceId\", \"testId1\");\n\n        verifyEvents(findEvents(query, new Date(0)));\n\n        verify(sdbMock, times(2)).select(arg.capture());\n        SelectRequest req = arg.getValue();\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"select * from `DOMAIN` where region = 'region'\");\n        sb.append(\" and instanceId = 'testId1'\");\n\n        Assert.assertEquals(req.getSelectExpression(), sb.toString() + \" and eventTime > '0' order by eventTime desc\");\n\n        // reset for next test\n        when(sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2);\n\n        verifyEvents(findEvents(Type.MONKEY, query, new Date(0)));\n\n        verify(sdbMock, times(4)).select(arg.capture());\n        req = arg.getValue();\n        sb.append(\" and monkeyType = 'MONKEY|com.netflix.simianarmy.aws.TestSimpleDBRecorder$Type'\");\n        Assert.assertEquals(req.getSelectExpression(), sb.toString() + \" and eventTime > '0' order by eventTime desc\");\n\n        // reset for next test\n        when(sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2);\n\n        verifyEvents(findEvents(Type.MONKEY, EventTypes.EVENT, query, new Date(0)));\n\n        verify(sdbMock, times(6)).select(arg.capture());\n        req = arg.getValue();\n        sb.append(\" and eventType = 'EVENT|com.netflix.simianarmy.aws.TestSimpleDBRecorder$EventTypes'\");\n        Assert.assertEquals(req.getSelectExpression(), sb.toString() + \" and eventTime > '0' order by eventTime desc\");\n\n        // reset for next test\n        when(sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2);\n\n        verifyEvents(findEvents(Type.MONKEY, EventTypes.EVENT, query, new Date(1330538400000L)));\n\n        verify(sdbMock, times(8)).select(arg.capture());\n        req = arg.getValue();\n        sb.append(\" and eventTime > '1330538400000' order by eventTime desc\");\n        Assert.assertEquals(req.getSelectExpression(), sb.toString());\n    }\n\n    void verifyEvents(List<Event> events) {\n        Assert.assertEquals(events.size(), 2);\n\n        Assert.assertEquals(events.get(0).id(), \"testId1\");\n        Assert.assertEquals(events.get(0).eventTime().getTime(), 1330538400000L);\n        Assert.assertEquals(events.get(0).monkeyType(), Type.MONKEY);\n        Assert.assertEquals(events.get(0).eventType(), EventTypes.EVENT);\n        Assert.assertEquals(events.get(0).field(\"field1\"), \"value1\");\n        Assert.assertEquals(events.get(0).field(\"field2\"), \"value2\");\n        Assert.assertEquals(events.get(0).fields().size(), 2);\n\n        Assert.assertEquals(events.get(1).id(), \"testId2\");\n        Assert.assertEquals(events.get(1).eventTime().getTime(), 1330538400000L);\n        Assert.assertEquals(events.get(1).monkeyType(), Type.MONKEY);\n        Assert.assertEquals(events.get(1).eventType(), EventTypes.EVENT);\n        Assert.assertEquals(events.get(1).field(\"field1\"), \"value1\");\n        Assert.assertEquals(events.get(1).field(\"field2\"), \"value2\");\n        Assert.assertEquals(events.get(1).fields().size(), 2);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/conformity/TestASGOwnerEmailTag.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.aws.conformity;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.aws.conformity.crawler.AWSClusterCrawler;\nimport com.netflix.simianarmy.basic.BasicConfiguration;\nimport com.netflix.simianarmy.basic.conformity.BasicConformityMonkeyContext;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport junit.framework.Assert;\nimport org.testng.annotations.Test;\n\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class TestASGOwnerEmailTag {\n    \n    private static final String ASG1 = \"asg1\";\n    private static final String ASG2 = \"asg2\";\n    private static final String OWNER_TAG_KEY = \"owner\";\n    private static final String OWNER_TAG_VALUE = \"tyler@paperstreet.com\";\n    private static final String REGION = \"eu-west-1\";\n\n    @Test\n    public void testForOwnerTag() {\n        Properties properties = new Properties();\n        BasicConformityMonkeyContext ctx = new BasicConformityMonkeyContext();\n\n        List<AutoScalingGroup> asgList = createASGList();\n        String[] asgNames = {ASG1, ASG2};\n\n        AWSClient awsMock = createMockAWSClient(asgList, asgNames);\n        Map<String, AWSClient> regionToAwsClient = Maps.newHashMap();\n        regionToAwsClient.put(\"us-east-1\", awsMock);\n        AWSClusterCrawler clusterCrawler = new AWSClusterCrawler(regionToAwsClient, new BasicConfiguration(properties));\n\n        List<Cluster> clusters = clusterCrawler.clusters(asgNames);\n        \n        Assert.assertTrue(OWNER_TAG_VALUE.equalsIgnoreCase(clusters.get(0).getOwnerEmail()));\n        Assert.assertNull(clusters.get(1).getOwnerEmail());\n    }\n\n    private List<AutoScalingGroup> createASGList() {\n        List<AutoScalingGroup> asgList = new LinkedList<AutoScalingGroup>();\n        asgList.add(makeASG(ASG1, OWNER_TAG_VALUE));\n        asgList.add(makeASG(ASG2, null));\n        return asgList;\n    }\n\n    private AutoScalingGroup makeASG(String asgName, String ownerEmail) {\n        TagDescription tag = new TagDescription().withKey(OWNER_TAG_KEY).withValue(ownerEmail);\n        AutoScalingGroup asg = new AutoScalingGroup()\n            .withAutoScalingGroupName(asgName)\n            .withTags(tag);\n        return asg;\n    }\n    \n    private AWSClient createMockAWSClient(List<AutoScalingGroup> asgList, String... asgNames) {\n        AWSClient awsMock = mock(AWSClient.class);\n        when(awsMock.describeAutoScalingGroups(asgNames)).thenReturn(asgList);\n        return awsMock;\n    }\n}"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/conformity/TestRDSConformityClusterTracker.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n// CHECKSTYLE IGNORE ParameterNumber\npackage com.netflix.simianarmy.aws.conformity;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\n\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Matchers;\nimport org.mockito.Mockito;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.core.RowMapper;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\n\npublic class TestRDSConformityClusterTracker extends RDSConformityClusterTracker {\n    public TestRDSConformityClusterTracker() {\n        super(mock(JdbcTemplate.class), \"conformitytable\");\n    }\n\n    @Test\n    public void testInit() {        \n    \tTestRDSConformityClusterTracker recorder = new TestRDSConformityClusterTracker();\t\n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        Mockito.doNothing().when(recorder.getJdbcTemplate()).execute(sqlCap.capture());\n        recorder.init();        \n        Assert.assertEquals(sqlCap.getValue(), \"create table if not exists conformitytable ( cluster varchar(255), region varchar(25), ownerEmail varchar(255), isConforming varchar(10), isOptedOut varchar(10), updateTimestamp BIGINT, excludedRules varchar(4096), conformities varchar(4096), conformityRules varchar(4096) )\");\n    }    \n\n    @SuppressWarnings(\"unchecked\")\n    @Test\n    public void testInsertNewCluster() {\n    \t// mock the select query that is issued to see if the record already exists\n        ArrayList<Cluster> clusters = new ArrayList<>();\n        TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker();\n\t\twhen(tracker.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                            Matchers.any(Object[].class), \n         \t\t                            Matchers.any(RowMapper.class))).thenReturn(clusters);\n    \t    \t\n        Cluster cluster1 = new Cluster(\"clustername1\", \"us-west-1\");\n        cluster1.setUpdateTime(new Date());\n        ArrayList<String> list = new ArrayList<>();\n        list.add(\"one\");\n        list.add(\"two\");\n        cluster1.updateConformity(new Conformity(\"rule1\",list));\n        list.add(\"three\");\n        cluster1.updateConformity(new Conformity(\"rule2\",list));\n\n        ArgumentCaptor<Object> objCap = ArgumentCaptor.forClass(Object.class);\n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        when(tracker.getJdbcTemplate().update(sqlCap.capture(), \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture())).thenReturn(1);\n        tracker.addOrUpdate(cluster1);\n\n        List<Object> args = objCap.getAllValues();        \n        Assert.assertEquals(args.size(), 9);\n        Assert.assertEquals(args.get(0).toString(), \"clustername1\");\n        Assert.assertEquals(args.get(1).toString(), \"us-west-1\");\n        Assert.assertEquals(args.get(7).toString(), \"{\\\"rule1\\\":\\\"one,two\\\",\\\"rule2\\\":\\\"one,two,three\\\"}\");\n        Assert.assertEquals(args.get(8).toString(), \"rule1,rule2\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Test\n    public void testUpdateCluster() {\n        Cluster cluster1 = new Cluster(\"clustername1\", \"us-west-1\");\n        Date date = new Date();\n        cluster1.setUpdateTime(date);\n\n        // mock the select query that is issued to see if the record already exists\n        ArrayList<Cluster> clusters = new ArrayList<>();\n        clusters.add(cluster1);\n        TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker();\n\t\twhen(tracker.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                            Matchers.any(Object[].class), \n         \t\t                            Matchers.any(RowMapper.class))).thenReturn(clusters);\n    \t    \t\n\t\tcluster1.setOwnerEmail(\"newemail@test.com\");\n        ArgumentCaptor<Object> objCap = ArgumentCaptor.forClass(Object.class);\n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        when(tracker.getJdbcTemplate().update(sqlCap.capture(), \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture())).thenReturn(1);\n        tracker.addOrUpdate(cluster1);\n\n        List<Object> args = objCap.getAllValues();        \n        Assert.assertEquals(sqlCap.getValue(), \"update conformitytable set ownerEmail=?,isConforming=?,isOptedOut=?,updateTimestamp=?,excludedRules=?,conformities=?,conformityRules=? where cluster=? and region=?\");\n        Assert.assertEquals(args.size(), 9);\n        Assert.assertEquals(args.get(0).toString(), \"newemail@test.com\");\n        Assert.assertEquals(args.get(1).toString(), \"false\");\n        Assert.assertEquals(args.get(2).toString(), \"false\");\n        Assert.assertEquals(args.get(3).toString(), date.getTime() + \"\");\n        Assert.assertEquals(args.get(7).toString(), \"clustername1\");\n        Assert.assertEquals(args.get(8).toString(), \"us-west-1\");\n    }\n    \n    @SuppressWarnings(\"unchecked\")\n\t@Test\n    public void testGetCluster() {\n        Cluster cluster1 = new Cluster(\"clustername1\", \"us-west-1\");\n       \n        ArrayList<Cluster> clusters = new ArrayList<>();\n        clusters.add(cluster1);\n        TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker();\n        \n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        \n\t\twhen(tracker.getJdbcTemplate().query(sqlCap.capture(), \n\t\t\t\t\t\t\t\t\t\t\t Matchers.any(Object[].class), \n         \t\t                             Matchers.any(RowMapper.class))).thenReturn(clusters);\n\t\tCluster result = tracker.getCluster(\"clustername1\", \"us-west-1\");\n\t\tAssert.assertNotNull(result);\n\t\tAssert.assertEquals(sqlCap.getValue(), \"select * from conformitytable where cluster = ? and region = ?\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n\t@Test\n    public void testGetClusters() {\n        Cluster cluster1 = new Cluster(\"clustername1\", \"us-west-1\");\n        Cluster cluster2 = new Cluster(\"clustername1\", \"us-west-2\");\n        Cluster cluster3 = new Cluster(\"clustername1\", \"us-east-1\");\n       \n        ArrayList<Cluster> clusters = new ArrayList<>();\n        clusters.add(cluster1);\n        clusters.add(cluster2);\n        clusters.add(cluster3);\n        TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker();\n        \n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        \n\t\twhen(tracker.getJdbcTemplate().query(sqlCap.capture(), \n         \t\t                             Matchers.any(RowMapper.class))).thenReturn(clusters);\n\t\tList<Cluster> results = tracker.getAllClusters(\"us-west-1\", \"us-west-2\", \"us-east-1\");\n\t\tAssert.assertEquals(results.size(), 3);\n\t\tAssert.assertEquals(sqlCap.getValue().toString().trim(), \"select * from conformitytable where cluster is not null and region in ('us-west-1','us-west-2','us-east-1')\");\n    }\n    \n\t@SuppressWarnings(\"unchecked\")\n\t@Test\n    public void testGetClusterNotFound() {\n        ArrayList<Cluster> clusters = new ArrayList<>();\n        TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker();\n        \n\t\twhen(tracker.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                             Matchers.any(Object[].class), \n         \t\t                             Matchers.any(RowMapper.class))).thenReturn(clusters);\n\t\tCluster cluster = tracker.getCluster(\"clustername\", \"us-west-1\");\n\t\tAssert.assertNull(cluster);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/conformity/rule/TestInstanceInVPC.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.aws.conformity.rule;\n\nimport com.amazonaws.services.ec2.model.Instance;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Sets;\nimport com.netflix.simianarmy.conformity.AutoScalingGroup;\nimport com.netflix.simianarmy.conformity.Cluster;\nimport com.netflix.simianarmy.conformity.Conformity;\nimport junit.framework.Assert;\nimport org.mockito.MockitoAnnotations;\nimport org.mockito.Spy;\nimport org.testng.annotations.BeforeMethod;\nimport org.testng.annotations.Test;\n\nimport java.util.List;\nimport java.util.Set;\n\nimport static org.mockito.Mockito.doReturn;\n\n\npublic class TestInstanceInVPC {\n    private static final String VPC_INSTANCE_ID = \"abc-123\";\n    private static final String INSTANCE_ID = \"zxy-098\";\n    private static final String REGION = \"eu-west-1\";\n\n    @Spy\n    private InstanceInVPC instanceInVPC = new InstanceInVPC();\n\n    @BeforeMethod\n    public void setUp() throws Exception {\n        MockitoAnnotations.initMocks(this);\n        List<Instance> instanceList = Lists.newArrayList();\n        Instance instance = new Instance().withInstanceId(VPC_INSTANCE_ID).withVpcId(\"12345\");\n        instanceList.add(instance);\n        doReturn(instanceList).when(instanceInVPC).getAWSInstances(REGION, VPC_INSTANCE_ID);\n        List<Instance> instanceList2 = Lists.newArrayList();\n        Instance instance2 = new Instance().withInstanceId(INSTANCE_ID);\n        instanceList2.add(instance2);\n        doReturn(instanceList2).when(instanceInVPC).getAWSInstances(REGION, INSTANCE_ID);\n\n    }\n\n    @Test\n    public void testCheckSoloInstances() throws Exception {\n        Set<String> list = Sets.newHashSet();\n        list.add(VPC_INSTANCE_ID);\n        list.add(INSTANCE_ID);\n        Cluster cluster = new Cluster(\"SoloInstances\", REGION, list);\n        Conformity result = instanceInVPC.check(cluster);\n        Assert.assertNotNull(result);\n        Assert.assertEquals(result.getRuleId(), instanceInVPC.getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 1);\n        Assert.assertEquals(result.getFailedComponents().iterator().next(), INSTANCE_ID);\n    }\n\n    @Test\n    public void testAsgInstances() throws Exception {\n        AutoScalingGroup autoScalingGroup = new AutoScalingGroup(\"Conforming\", VPC_INSTANCE_ID);\n        Cluster conformingCluster = new Cluster(\"Conforming\", REGION, autoScalingGroup);\n        Conformity result = instanceInVPC.check(conformingCluster);\n        Assert.assertNotNull(result);\n        Assert.assertEquals(result.getRuleId(), instanceInVPC.getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 0);\n\n        autoScalingGroup = new AutoScalingGroup(\"NonConforming\", INSTANCE_ID);\n        Cluster nonConformingCluster = new Cluster(\"NonConforming\", REGION, autoScalingGroup);\n        result = instanceInVPC.check(nonConformingCluster);\n        Assert.assertNotNull(result);\n        Assert.assertEquals(result.getRuleId(), instanceInVPC.getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 1);\n        Assert.assertEquals(result.getFailedComponents().iterator().next(), INSTANCE_ID);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/TestAWSResource.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\npackage com.netflix.simianarmy.aws.janitor;\n\nimport java.lang.reflect.Field;\nimport java.util.Collection;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\n\npublic class TestAWSResource {\n    /** Make sure the getFieldToValue returns the right field and values.\n     * @throws Exception **/\n    @Test\n    public void testFieldToValueMapWithoutNullForInstance() throws Exception {\n        Date now = new Date();\n        Resource resource = getTestingResource(now);\n\n        Map<String, String> resourceFieldValueMap = resource.getFieldToValueMap();\n\n        verifyMapsAreEqual(resourceFieldValueMap,\n                getTestingFieldValueMap(now, getTestingFields()));\n    }\n\n    /**\n     * When all fields are null, the map returned is empty.\n     */\n    @Test\n    public void testFieldToValueMapWithNull() {\n        Resource resource = new AWSResource();\n        Map<String, String> resourceFieldValueMap = resource.getFieldToValueMap();\n        // The only value in the map is the boolean of opt out\n        Assert.assertEquals(resourceFieldValueMap.size(), 1);\n    }\n\n    @Test\n    public void testParseFieldToValueMap() throws Exception {\n        Date now = new Date();\n        Map<String, String> map = getTestingFieldValueMap(now, getTestingFields());\n        AWSResource resource = AWSResource.parseFieldtoValueMap(map);\n\n        Map<String, String> resourceFieldValueMap = resource.getFieldToValueMap();\n\n        verifyMapsAreEqual(resourceFieldValueMap, map);\n    }\n\n    @Test\n    public void testClone() {\n        Date now = new Date();\n        Resource resource = getTestingResource(now);\n        Resource clone = resource.cloneResource();\n        verifyMapsAreEqual(clone.getFieldToValueMap(), resource.getFieldToValueMap());\n        verifyTagsAreEqual(clone, resource);\n    }\n\n    private void verifyMapsAreEqual(Map<String, String> map1, Map<String, String> map2) {\n        Assert.assertFalse(map1 == null ^ map2 == null);\n        Assert.assertEquals(map1.size(), map2.size());\n        for (Map.Entry<String, String> entry : map1.entrySet()) {\n            Assert.assertEquals(entry.getValue(), map2.get(entry.getKey()));\n        }\n    }\n\n    private void verifyTagsAreEqual(Resource r1, Resource r2) {\n        Collection<String> keys1 = r1.getAllTagKeys();\n        Collection<String> keys2 = r2.getAllTagKeys();\n        Assert.assertEquals(keys1.size(), keys2.size());\n\n        for (String key : keys1) {\n            Assert.assertEquals(r1.getTag(key), r2.getTag(key));\n        }\n    }\n\n    private Map<String, String> getTestingFieldValueMap(Date defaultDate, Map<String, String> additionalFields)\n            throws Exception {\n        Field[] fields = AWSResource.class.getFields();\n        Map<String, String> fieldToValue = new HashMap<String, String>();\n\n        String dateString = AWSResource.DATE_FORMATTER.print(defaultDate.getTime());\n        for (Field field : fields) {\n            if (field.getName().startsWith(\"FIELD_\")) {\n                String value;\n                String key = (String) (field.get(null));\n                if (field.getName().endsWith(\"TIME\")) {\n                    value = dateString;\n                } else if (field.getName().equals(\"FIELD_STATE\")) {\n                    value = \"MARKED\";\n                } else if (field.getName().equals(\"FIELD_RESOURCE_TYPE\")) {\n                    value = \"INSTANCE\";\n                } else if (field.getName().equals(\"FIELD_OPT_OUT_OF_JANITOR\")) {\n                    value = \"false\";\n                } else {\n                    value = (String) (field.get(null));\n                }\n                fieldToValue.put(key, value);\n            }\n        }\n        if (additionalFields != null) {\n            fieldToValue.putAll(additionalFields);\n        }\n        return fieldToValue;\n    }\n\n    private Resource getTestingResource(Date now) {\n        String id = \"resourceId\";\n        Resource resource = new AWSResource().withId(id).withRegion(\"region\").withResourceType(AWSResourceType.INSTANCE)\n                .withState(Resource.CleanupState.MARKED).withDescription(\"description\")\n                .withExpectedTerminationTime(now).withActualTerminationTime(now)\n                .withLaunchTime(now).withMarkTime(now).withNnotificationTime(now).withOwnerEmail(\"ownerEmail\")\n                .withTerminationReason(\"terminationReason\").withOptOutOfJanitor(false);\n        ((AWSResource) resource).setAWSResourceState(\"awsResourceState\");\n\n        for (Map.Entry<String, String> field : getTestingFields().entrySet()) {\n            resource.setAdditionalField(field.getKey(), field.getValue());\n        }\n\n        for (int i = 1; i < 10; i++) {\n            resource.setTag(\"tagKey_\" + i, \"tagValue_\" + i);\n        }\n\n        return resource;\n    }\n\n    private Map<String, String> getTestingFields() {\n        Map<String, String> map = new HashMap<String, String>();\n        for (int i = 0; i < 10; i++) {\n            map.put(\"name\" + i, \"value\" + i);\n        }\n        return map;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/TestRDSJanitorResourceTracker.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n// CHECKSTYLE IGNORE ParameterNumber\npackage com.netflix.simianarmy.aws.janitor;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport org.joda.time.DateTime;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Matchers;\nimport org.mockito.Mockito;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.core.RowMapper;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport java.util.*;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class TestRDSJanitorResourceTracker extends RDSJanitorResourceTracker {\n\t\n    public TestRDSJanitorResourceTracker() {\n        super(mock(JdbcTemplate.class), \"janitortable\");\n    }\n\n    @Test\n    public void testInit() {        \n    \tTestRDSJanitorResourceTracker recorder = new TestRDSJanitorResourceTracker();\t\n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        Mockito.doNothing().when(recorder.getJdbcTemplate()).execute(sqlCap.capture());\n        recorder.init();        \n        Assert.assertEquals(sqlCap.getValue(), \"create table if not exists janitortable ( resourceId varchar(255),  resourceType varchar(255),  region varchar(25),  ownerEmail varchar(255),  description varchar(255),  state varchar(25),  terminationReason varchar(255),  expectedTerminationTime BIGINT,  actualTerminationTime BIGINT,  notificationTime BIGINT,  launchTime BIGINT,  markTime BIGINT,  optOutOfJanitor varchar(8),  additionalFields varchar(4096) )\");\n    }    \n\n    @SuppressWarnings(\"unchecked\")\n    @Test\n    public void testInsertNewResource() {\n    \t// mock the select query that is issued to see if the record already exists\n        ArrayList<AWSResource> resources = new ArrayList<>();\n        TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker();\n\t\twhen(tracker.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                            Matchers.any(Object[].class), \n         \t\t                            Matchers.any(RowMapper.class))).thenReturn(resources);\n    \t    \t\n        String id = \"i-12345678901234567\";\n        AWSResourceType resourceType = AWSResourceType.INSTANCE;\n        Resource.CleanupState state = Resource.CleanupState.MARKED;\n        String description = \"This is a test resource.\";\n        String ownerEmail = \"owner@test.com\";\n        String region = \"us-east-1\";\n        String terminationReason = \"This is a test termination reason.\";\n        DateTime now = DateTime.now();\n        Date expectedTerminationTime = new Date(now.plusDays(10).getMillis());\n        Date markTime = new Date(now.getMillis());\n        String fieldName = \"fieldName123\";\n        String fieldValue = \"fieldValue456\";\n        Resource resource = new AWSResource().withId(id).withResourceType(resourceType)\n                .withDescription(description).withOwnerEmail(ownerEmail).withRegion(region)\n                .withState(state).withTerminationReason(terminationReason)\n                .withExpectedTerminationTime(expectedTerminationTime)\n                .withMarkTime(markTime).withOptOutOfJanitor(false)\n                .setAdditionalField(fieldName, fieldValue);\n\n        ArgumentCaptor<Object> objCap = ArgumentCaptor.forClass(Object.class);\n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        when(tracker.getJdbcTemplate().update(sqlCap.capture(), \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),\n                                              objCap.capture(),\n        \t\t                              objCap.capture(),\n                                              objCap.capture(),\n        \t\t                              objCap.capture())).thenReturn(1);\n        tracker.addOrUpdate(resource);\n\n        List<Object> args = objCap.getAllValues();\n\n        Assert.assertEquals(sqlCap.getValue(), \"insert into janitortable (resourceId,resourceType,region,ownerEmail,description,state,terminationReason,expectedTerminationTime,actualTerminationTime,notificationTime,launchTime,markTime,optOutOfJanitor,additionalFields) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?)\");\n        Assert.assertEquals(args.size(), 14);\n        Assert.assertEquals(args.get(0).toString(), id);\n        Assert.assertEquals(args.get(1).toString(), AWSResourceType.INSTANCE.toString());\n        Assert.assertEquals(args.get(2).toString(), region);\n        Assert.assertEquals(args.get(3).toString(), ownerEmail);\n        Assert.assertEquals(args.get(4).toString(), description);\n        Assert.assertEquals(args.get(5).toString(), state.toString());\n        Assert.assertEquals(args.get(6).toString(), terminationReason);\n        Assert.assertEquals(args.get(7).toString(), expectedTerminationTime.getTime() + \"\");\n        Assert.assertEquals(args.get(8).toString(), \"0\");\n        Assert.assertEquals(args.get(9).toString(), \"0\");\n        Assert.assertEquals(args.get(10).toString(), \"0\");\n        Assert.assertEquals(args.get(11).toString(), markTime.getTime() + \"\");\n        Assert.assertEquals(args.get(12).toString(), \"false\");\n        Assert.assertEquals(args.get(13).toString(), \"{\\\"fieldName123\\\":\\\"fieldValue456\\\"}\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Test\n    public void testUpdateResource() {\n        String id = \"i-12345678901234567\";\n        AWSResourceType resourceType = AWSResourceType.INSTANCE;\n        Resource.CleanupState state = Resource.CleanupState.MARKED;\n        String description = \"This is a test resource.\";\n        String ownerEmail = \"owner@test.com\";\n        String region = \"us-east-1\";\n        String terminationReason = \"This is a test termination reason.\";\n        DateTime now = DateTime.now();\n        Date expectedTerminationTime = new Date(now.plusDays(10).getMillis());\n        Date markTime = new Date(now.getMillis());\n        String fieldName = \"fieldName123\";\n        String fieldValue = \"fieldValue456\";\n        Resource resource = new AWSResource().withId(id).withResourceType(resourceType)\n                .withDescription(description).withOwnerEmail(ownerEmail).withRegion(region)\n                .withState(state).withTerminationReason(terminationReason)\n                .withExpectedTerminationTime(expectedTerminationTime)\n                .withMarkTime(markTime).withOptOutOfJanitor(false)\n                .setAdditionalField(fieldName, fieldValue);\n        \n        // mock the select query that is issued to see if the record already exists\n        ArrayList<Resource> resources = new ArrayList<>();\n        resources.add(resource);\n        TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker();\n\t\twhen(tracker.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                            Matchers.any(Object[].class), \n         \t\t                            Matchers.any(RowMapper.class))).thenReturn(resources);\n\n\t\t// update the ownerEmail\n\t\townerEmail = \"owner2@test.com\";\n        Resource newResource = new AWSResource().withId(id).withResourceType(resourceType)\n                .withDescription(description).withOwnerEmail(ownerEmail).withRegion(region)\n                .withState(state).withTerminationReason(terminationReason)\n                .withExpectedTerminationTime(expectedTerminationTime)\n                .withMarkTime(markTime).withOptOutOfJanitor(false)\n                .setAdditionalField(fieldName, fieldValue);\n\t\t\n        ArgumentCaptor<Object> objCap = ArgumentCaptor.forClass(Object.class);\n        ArgumentCaptor<String> sqlCap = ArgumentCaptor.forClass(String.class);\n        when(tracker.getJdbcTemplate().update(sqlCap.capture(), \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),\n                                              objCap.capture(),\n                                              objCap.capture(),\n        \t\t                              objCap.capture(),  \n        \t\t                              objCap.capture(),\n                                              objCap.capture(),\n                                              objCap.capture(),\n                                              objCap.capture())).thenReturn(1);\n        tracker.addOrUpdate(newResource);\n\n        List<Object> args = objCap.getAllValues();        \n        Assert.assertEquals(sqlCap.getValue(), \"update janitortable set resourceType=?,region=?,ownerEmail=?,description=?,state=?,terminationReason=?,expectedTerminationTime=?,actualTerminationTime=?,notificationTime=?,launchTime=?,markTime=?,optOutOfJanitor=?,additionalFields=? where resourceId=? and region=?\");\n        Assert.assertEquals(args.size(), 15);\n        Assert.assertEquals(args.get(0).toString(), AWSResourceType.INSTANCE.toString());\n        Assert.assertEquals(args.get(1).toString(), region);\n        Assert.assertEquals(args.get(2).toString(), ownerEmail);\n        Assert.assertEquals(args.get(3).toString(), description);\n        Assert.assertEquals(args.get(4).toString(), state.toString());\n        Assert.assertEquals(args.get(5).toString(), terminationReason);\n        Assert.assertEquals(args.get(6).toString(), expectedTerminationTime.getTime() + \"\");\n        Assert.assertEquals(args.get(7).toString(), \"0\");\n        Assert.assertEquals(args.get(8).toString(), \"0\");\n        Assert.assertEquals(args.get(9).toString(), \"0\");\n        Assert.assertEquals(args.get(10).toString(), markTime.getTime() + \"\");\n        Assert.assertEquals(args.get(11).toString(), \"false\");\n        Assert.assertEquals(args.get(12).toString(), \"{\\\"fieldName123\\\":\\\"fieldValue456\\\"}\");\n        Assert.assertEquals(args.get(13).toString(), id);\n        Assert.assertEquals(args.get(14).toString(), region);\n    }\n\n    \n    @SuppressWarnings(\"unchecked\")\n\t@Test\n    public void testGetResource() {\n        String id1 = \"id-1\";\n        AWSResourceType resourceType = AWSResourceType.INSTANCE;\n        Resource.CleanupState state = Resource.CleanupState.MARKED;\n        String description = \"This is a test resource.\";\n        String ownerEmail = \"owner@test.com\";\n        String region = \"us-east-1\";\n        String terminationReason = \"This is a test termination reason.\";\n        DateTime now = DateTime.now();\n        Date expectedTerminationTime = new Date(now.plusDays(10).getMillis());\n        Date markTime = new Date(now.getMillis());\n        String fieldName = \"fieldName123\";\n        String fieldValue = \"fieldValue456\";\n                \n        AWSResource result1 = mkResource(id1, resourceType, state, description, ownerEmail,\n                region, terminationReason, expectedTerminationTime, markTime, false, fieldName, fieldValue);\n\n        ArrayList<AWSResource> resources = new ArrayList<>();\n        resources.add(result1);\n        TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker();\n\t\twhen(tracker.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                            Matchers.any(Object[].class), \n         \t\t                            Matchers.any(RowMapper.class))).thenReturn(resources);\n\n\t\tResource resource = tracker.getResource(id1);\n        ArrayList<Resource> returnResources = new ArrayList<>();\n        returnResources.add(resource);\n\n        verifyResources(returnResources,\n                id1, null, resourceType, state, description, ownerEmail,\n                region, terminationReason, expectedTerminationTime, markTime, fieldName, fieldValue);\n    }\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Test\n    public void testGetResourceNotFound() {\n        ArrayList<AWSResource> resources = new ArrayList<>();\n        TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker();\n\t\twhen(tracker.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                            Matchers.any(Object[].class), \n         \t\t                            Matchers.any(RowMapper.class))).thenReturn(resources);\n\t\tResource resource = tracker.getResource(\"id-2\");\n\t\tAssert.assertNull(resource);\n    }\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Test\n    public void testGetResources() {\n        String id1 = \"id-1\";\n        String id2 = \"id-2\";\n        AWSResourceType resourceType = AWSResourceType.INSTANCE;\n        Resource.CleanupState state = Resource.CleanupState.MARKED;\n        String description = \"This is a test resource.\";\n        String ownerEmail = \"owner@test.com\";\n        String region = \"us-east-1\";\n        String terminationReason = \"This is a test termination reason.\";\n        DateTime now = DateTime.now();\n        Date expectedTerminationTime = new Date(now.plusDays(10).getMillis());\n        Date markTime = new Date(now.getMillis());\n        String fieldName = \"fieldName123\";\n        String fieldValue = \"fieldValue456\";\n                \n        AWSResource result1 = mkResource(id1, resourceType, state, description, ownerEmail,\n                region, terminationReason, expectedTerminationTime, markTime, false, fieldName, fieldValue);\n        AWSResource result2 = mkResource(id2, resourceType, state, description, ownerEmail,\n                region, terminationReason, expectedTerminationTime, markTime, true, fieldName, fieldValue);\n\n        ArrayList<AWSResource> resources = new ArrayList<>();\n        resources.add(result1);\n        resources.add(result2);\n        TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker();\n\t\twhen(tracker.getJdbcTemplate().query(Matchers.anyString(), \n         \t\t                            Matchers.any(Object[].class), \n         \t\t                            Matchers.any(RowMapper.class))).thenReturn(resources);\n\n        verifyResources(tracker.getResources(resourceType, state, region),\n                id1, id2, resourceType, state, description, ownerEmail,\n                region, terminationReason, expectedTerminationTime, markTime, fieldName, fieldValue);\n\n    }\n\n    private void verifyResources(List<Resource> resources, String id1, String id2, AWSResourceType resourceType,\n            Resource.CleanupState state, String description, String ownerEmail, String region,\n            String terminationReason, Date expectedTerminationTime, Date markTime, String fieldName,\n            String fieldValue) {\n    \t\n    \tif (id2 == null) {\n    \t\tAssert.assertEquals(resources.size(), 1);    \t\t\n    \t} else {\n    \t\tAssert.assertEquals(resources.size(), 2);\n    \t}\n\n        Assert.assertEquals(resources.get(0).getId(), id1);\n        Assert.assertEquals(resources.get(0).getResourceType(), resourceType);\n        Assert.assertEquals(resources.get(0).getState(), state);\n        Assert.assertEquals(resources.get(0).getDescription(), description);\n        Assert.assertEquals(resources.get(0).getOwnerEmail(), ownerEmail);\n        Assert.assertEquals(resources.get(0).getRegion(), region);\n        Assert.assertEquals(resources.get(0).getTerminationReason(), terminationReason);\n        Assert.assertEquals(\n                AWSResource.DATE_FORMATTER.print(resources.get(0).getExpectedTerminationTime().getTime()),\n                AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime()));\n        Assert.assertEquals(\n                AWSResource.DATE_FORMATTER.print(resources.get(0).getMarkTime().getTime()),\n                AWSResource.DATE_FORMATTER.print(markTime.getTime()));\n        Assert.assertEquals(resources.get(0).getAdditionalField(fieldName), fieldValue);\n        Assert.assertEquals(resources.get(0).isOptOutOfJanitor(), false);\n\n        if (id2 != null) {\n\t        Assert.assertEquals(resources.get(1).getId(), id2);\n\t        Assert.assertEquals(resources.get(1).getResourceType(), resourceType);\n\t        Assert.assertEquals(resources.get(1).getState(), state);\n\t        Assert.assertEquals(resources.get(1).getDescription(), description);\n\t        Assert.assertEquals(resources.get(1).getOwnerEmail(), ownerEmail);\n\t        Assert.assertEquals(resources.get(1).getRegion(), region);\n\t        Assert.assertEquals(resources.get(1).getTerminationReason(), terminationReason);\n\t        Assert.assertEquals(\n\t                AWSResource.DATE_FORMATTER.print(resources.get(1).getExpectedTerminationTime().getTime()),\n\t                AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime()));\n\t        Assert.assertEquals(\n\t                AWSResource.DATE_FORMATTER.print(resources.get(1).getMarkTime().getTime()),\n\t                AWSResource.DATE_FORMATTER.print(markTime.getTime()));\n\t        Assert.assertEquals(resources.get(1).isOptOutOfJanitor(), true);\n\t        Assert.assertEquals(resources.get(1).getAdditionalField(fieldName), fieldValue);\n        }\n    }\n\n    private AWSResource mkResource(String id, AWSResourceType resourceType, Resource.CleanupState state,\n            String description, String ownerEmail, String region, String terminationReason,\n            Date expectedTerminationTime, Date markTime, boolean optOut, String fieldName, String fieldValue) {\n\n    \tMap<String, String> attrs = new HashMap<>();\n        attrs.put(AWSResource.FIELD_RESOURCE_ID, id);\n        attrs.put(AWSResource.FIELD_RESOURCE_TYPE, resourceType.name());\n        attrs.put(AWSResource.FIELD_DESCRIPTION, description);\n        attrs.put(AWSResource.FIELD_REGION, region);\n        attrs.put(AWSResource.FIELD_STATE, state.name());\n        attrs.put(AWSResource.FIELD_OWNER_EMAIL, ownerEmail);\n        attrs.put(AWSResource.FIELD_TERMINATION_REASON, terminationReason);\n        attrs.put(AWSResource.FIELD_EXPECTED_TERMINATION_TIME,\n                AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime()));\n        attrs.put(AWSResource.FIELD_MARK_TIME,\n                AWSResource.DATE_FORMATTER.print(markTime.getTime()));\n        attrs.put(AWSResource.FIELD_OPT_OUT_OF_JANITOR, String.valueOf(optOut));\n        attrs.put(fieldName, fieldValue);\n\n        return AWSResource.parseFieldtoValueMap(attrs);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/TestSimpleDBJanitorResourceTracker.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n// CHECKSTYLE IGNORE ParameterNumber\npackage com.netflix.simianarmy.aws.janitor;\n\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Arrays;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.joda.time.DateTime;\nimport org.mockito.ArgumentCaptor;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.amazonaws.services.simpledb.AmazonSimpleDB;\nimport com.amazonaws.services.simpledb.model.Attribute;\nimport com.amazonaws.services.simpledb.model.Item;\nimport com.amazonaws.services.simpledb.model.PutAttributesRequest;\nimport com.amazonaws.services.simpledb.model.ReplaceableAttribute;\nimport com.amazonaws.services.simpledb.model.SelectRequest;\nimport com.amazonaws.services.simpledb.model.SelectResult;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\npublic class TestSimpleDBJanitorResourceTracker extends SimpleDBJanitorResourceTracker {\n\n    private static AWSClient makeMockAWSClient() {\n        AmazonSimpleDB sdbMock = mock(AmazonSimpleDB.class);\n        AWSClient awsClient = mock(AWSClient.class);\n        when(awsClient.sdbClient()).thenReturn(sdbMock);\n        return awsClient;\n    }\n\n    public TestSimpleDBJanitorResourceTracker() {\n        super(makeMockAWSClient(), \"DOMAIN\");\n        sdbMock = super.getSimpleDBClient();\n    }\n\n    private final AmazonSimpleDB sdbMock;\n\n    @Test\n    public void testAddResource() {\n        String id = \"i-12345678901234567\";\n        AWSResourceType resourceType = AWSResourceType.INSTANCE;\n        Resource.CleanupState state = Resource.CleanupState.MARKED;\n        String description = \"This is a test resource.\";\n        String ownerEmail = \"owner@test.com\";\n        String region = \"us-east-1\";\n        String terminationReason = \"This is a test termination reason.\";\n        DateTime now = DateTime.now();\n        Date expectedTerminationTime = new Date(now.plusDays(10).getMillis());\n        Date markTime = new Date(now.getMillis());\n        String fieldName = \"fieldName123\";\n        String fieldValue = \"fieldValue456\";\n        Resource resource = new AWSResource().withId(id).withResourceType(resourceType)\n                .withDescription(description).withOwnerEmail(ownerEmail).withRegion(region)\n                .withState(state).withTerminationReason(terminationReason)\n                .withExpectedTerminationTime(expectedTerminationTime)\n                .withMarkTime(markTime).withOptOutOfJanitor(false)\n                .setAdditionalField(fieldName, fieldValue);\n        ArgumentCaptor<PutAttributesRequest> arg = ArgumentCaptor.forClass(PutAttributesRequest.class);\n\n        TestSimpleDBJanitorResourceTracker tracker = new TestSimpleDBJanitorResourceTracker();\n\n        tracker.addOrUpdate(resource);\n        verify(tracker.sdbMock).putAttributes(arg.capture());\n        PutAttributesRequest req = arg.getValue();\n\n        Assert.assertEquals(req.getDomainName(), \"DOMAIN\");\n        Assert.assertEquals(req.getItemName(), getSimpleDBItemName(resource));\n        Map<String, String> map = new HashMap<String, String>();\n        for (ReplaceableAttribute attr : req.getAttributes()) {\n            map.put(attr.getName(), attr.getValue());\n        }\n\n        Assert.assertEquals(map.remove(AWSResource.FIELD_RESOURCE_ID), id);\n        Assert.assertEquals(map.remove(AWSResource.FIELD_DESCRIPTION), description);\n        Assert.assertEquals(map.remove(AWSResource.FIELD_EXPECTED_TERMINATION_TIME),\n                AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime()));\n        Assert.assertEquals(map.remove(AWSResource.FIELD_MARK_TIME),\n                AWSResource.DATE_FORMATTER.print(markTime.getTime()));\n        Assert.assertEquals(map.remove(AWSResource.FIELD_REGION), region);\n        Assert.assertEquals(map.remove(AWSResource.FIELD_OWNER_EMAIL), ownerEmail);\n        Assert.assertEquals(map.remove(AWSResource.FIELD_RESOURCE_TYPE), resourceType.name());\n        Assert.assertEquals(map.remove(AWSResource.FIELD_STATE), state.name());\n        Assert.assertEquals(map.remove(AWSResource.FIELD_TERMINATION_REASON), terminationReason);\n        Assert.assertEquals(map.remove(AWSResource.FIELD_OPT_OUT_OF_JANITOR), \"false\");\n        Assert.assertEquals(map.remove(fieldName), fieldValue);\n        Assert.assertEquals(map.size(), 0);\n    }\n\n\n    @Test\n    public void testGetResources() {\n        String id1 = \"id-1\";\n        String id2 = \"id-2\";\n        AWSResourceType resourceType = AWSResourceType.INSTANCE;\n        Resource.CleanupState state = Resource.CleanupState.MARKED;\n        String description = \"This is a test resource.\";\n        String ownerEmail = \"owner@test.com\";\n        String region = \"us-east-1\";\n        String terminationReason = \"This is a test termination reason.\";\n        DateTime now = DateTime.now();\n        Date expectedTerminationTime = new Date(now.plusDays(10).getMillis());\n        Date markTime = new Date(now.getMillis());\n        String fieldName = \"fieldName123\";\n        String fieldValue = \"fieldValue456\";\n\n        SelectResult result1 = mkSelectResult(id1, resourceType, state, description, ownerEmail,\n                region, terminationReason, expectedTerminationTime, markTime, false, fieldName, fieldValue);\n        result1.setNextToken(\"nextToken\");\n        SelectResult result2 = mkSelectResult(id2, resourceType, state, description, ownerEmail,\n                region, terminationReason, expectedTerminationTime, markTime, true, fieldName, fieldValue);\n\n        ArgumentCaptor<SelectRequest> arg = ArgumentCaptor.forClass(SelectRequest.class);\n\n        TestSimpleDBJanitorResourceTracker tracker = new TestSimpleDBJanitorResourceTracker();\n\n        when(tracker.sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2);\n\n        verifyResources(tracker.getResources(resourceType, state, region),\n                id1, id2, resourceType, state, description, ownerEmail,\n                region, terminationReason, expectedTerminationTime, markTime, fieldName, fieldValue);\n\n        verify(tracker.sdbMock, times(2)).select(arg.capture());\n    }\n\n    private void verifyResources(List<Resource> resources, String id1, String id2, AWSResourceType resourceType,\n            Resource.CleanupState state, String description, String ownerEmail, String region,\n            String terminationReason, Date expectedTerminationTime, Date markTime, String fieldName,\n            String fieldValue) {\n        Assert.assertEquals(resources.size(), 2);\n\n        Assert.assertEquals(resources.get(0).getId(), id1);\n        Assert.assertEquals(resources.get(0).getResourceType(), resourceType);\n        Assert.assertEquals(resources.get(0).getState(), state);\n        Assert.assertEquals(resources.get(0).getDescription(), description);\n        Assert.assertEquals(resources.get(0).getOwnerEmail(), ownerEmail);\n        Assert.assertEquals(resources.get(0).getRegion(), region);\n        Assert.assertEquals(resources.get(0).getTerminationReason(), terminationReason);\n        Assert.assertEquals(\n                AWSResource.DATE_FORMATTER.print(resources.get(0).getExpectedTerminationTime().getTime()),\n                AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime()));\n        Assert.assertEquals(\n                AWSResource.DATE_FORMATTER.print(resources.get(0).getMarkTime().getTime()),\n                AWSResource.DATE_FORMATTER.print(markTime.getTime()));\n        Assert.assertEquals(resources.get(0).getAdditionalField(fieldName), fieldValue);\n        Assert.assertEquals(resources.get(0).isOptOutOfJanitor(), false);\n\n        Assert.assertEquals(resources.get(1).getId(), id2);\n        Assert.assertEquals(resources.get(1).getResourceType(), resourceType);\n        Assert.assertEquals(resources.get(1).getState(), state);\n        Assert.assertEquals(resources.get(1).getDescription(), description);\n        Assert.assertEquals(resources.get(1).getOwnerEmail(), ownerEmail);\n        Assert.assertEquals(resources.get(1).getRegion(), region);\n        Assert.assertEquals(resources.get(1).getTerminationReason(), terminationReason);\n        Assert.assertEquals(\n                AWSResource.DATE_FORMATTER.print(resources.get(1).getExpectedTerminationTime().getTime()),\n                AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime()));\n        Assert.assertEquals(\n                AWSResource.DATE_FORMATTER.print(resources.get(1).getMarkTime().getTime()),\n                AWSResource.DATE_FORMATTER.print(markTime.getTime()));\n        Assert.assertEquals(resources.get(1).isOptOutOfJanitor(), true);\n        Assert.assertEquals(resources.get(1).getAdditionalField(fieldName), fieldValue);\n    }\n\n    private SelectResult mkSelectResult(String id, AWSResourceType resourceType, Resource.CleanupState state,\n            String description, String ownerEmail, String region, String terminationReason,\n            Date expectedTerminationTime, Date markTime, boolean optOut, String fieldName, String fieldValue) {\n        Item item = new Item();\n        List<Attribute> attrs = new LinkedList<Attribute>();\n        attrs.add(new Attribute(AWSResource.FIELD_RESOURCE_ID, id));\n        attrs.add(new Attribute(AWSResource.FIELD_RESOURCE_TYPE, resourceType.name()));\n        attrs.add(new Attribute(AWSResource.FIELD_DESCRIPTION, description));\n        attrs.add(new Attribute(AWSResource.FIELD_REGION, region));\n        attrs.add(new Attribute(AWSResource.FIELD_STATE, state.name()));\n        attrs.add(new Attribute(AWSResource.FIELD_OWNER_EMAIL, ownerEmail));\n        attrs.add(new Attribute(AWSResource.FIELD_TERMINATION_REASON, terminationReason));\n        attrs.add(new Attribute(AWSResource.FIELD_EXPECTED_TERMINATION_TIME,\n                AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime())));\n        attrs.add(new Attribute(AWSResource.FIELD_MARK_TIME,\n                AWSResource.DATE_FORMATTER.print(markTime.getTime())));\n        attrs.add(new Attribute(AWSResource.FIELD_OPT_OUT_OF_JANITOR, String.valueOf(optOut)));\n        attrs.add(new Attribute(fieldName, fieldValue));\n\n        item.setAttributes(attrs);\n        item.setName(String.format(\"%s-%s-%s\", resourceType.name(), id, region));\n        SelectResult result = new SelectResult();\n        result.setItems(Arrays.asList(item));\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestASGJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.ArrayList;\nimport java.util.EnumSet;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.SuspendedProcess;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\npublic class TestASGJanitorCrawler {\n\n    @Test\n    public void testResourceTypes() {\n        ASGJanitorCrawler crawler = new ASGJanitorCrawler(createMockAWSClient(createASGList()));\n        EnumSet<?> types = crawler.resourceTypes();\n        Assert.assertEquals(types.size(), 1);\n        Assert.assertEquals(types.iterator().next().name(), \"ASG\");\n    }\n\n    @Test\n    public void testInstancesWithNullNames() {\n        List<AutoScalingGroup> asgList = createASGList();\n        AWSClient awsMock = createMockAWSClient(asgList);\n        ASGJanitorCrawler crawler = new ASGJanitorCrawler(awsMock);\n        List<Resource> resources = crawler.resources();\n        verifyASGList(resources, asgList);\n    }\n\n    @Test\n    public void testInstancesWithNames() {\n        List<AutoScalingGroup> asgList = createASGList();\n        String[] asgNames = {\"asg1\", \"asg2\"};\n        AWSClient awsMock = createMockAWSClient(asgList, asgNames);\n        ASGJanitorCrawler crawler = new ASGJanitorCrawler(awsMock);\n        List<Resource> resources = crawler.resources(asgNames);\n        verifyASGList(resources, asgList);\n    }\n\n    @Test\n    public void testInstancesWithResourceType() {\n        List<AutoScalingGroup> asgList = createASGList();\n        AWSClient awsMock = createMockAWSClient(asgList);\n        ASGJanitorCrawler crawler = new ASGJanitorCrawler(awsMock);\n        for (AWSResourceType resourceType : AWSResourceType.values()) {\n            List<Resource> resources = crawler.resources(resourceType);\n            if (resourceType == AWSResourceType.ASG) {\n                verifyASGList(resources, asgList);\n            } else {\n                Assert.assertTrue(resources.isEmpty());\n            }\n        }\n    }\n\n    private void verifyASGList(List<Resource> resources, List<AutoScalingGroup> asgList) {\n        Assert.assertEquals(resources.size(), asgList.size());\n        for (int i = 0; i < resources.size(); i++) {\n            AutoScalingGroup asg = asgList.get(i);\n            verifyASG(resources.get(i), asg.getAutoScalingGroupName());\n        }\n    }\n\n    private void verifyASG(Resource asg, String asgName) {\n        Assert.assertEquals(asg.getResourceType(), AWSResourceType.ASG);\n        Assert.assertEquals(asg.getId(), asgName);\n        Assert.assertEquals(asg.getRegion(), \"us-east-1\");\n        Assert.assertEquals(asg.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME),\n                \"2012-12-03T23:00:03\");\n    }\n\n    private AWSClient createMockAWSClient(List<AutoScalingGroup> asgList, String... asgNames) {\n        AWSClient awsMock = mock(AWSClient.class);\n        when(awsMock.describeAutoScalingGroups(asgNames)).thenReturn(asgList);\n        when(awsMock.region()).thenReturn(\"us-east-1\");\n        return awsMock;\n    }\n\n    private List<AutoScalingGroup> createASGList() {\n        List<AutoScalingGroup> asgList = new LinkedList<AutoScalingGroup>();\n        asgList.add(mkASG(\"asg1\"));\n        asgList.add(mkASG(\"asg2\"));\n        return asgList;\n    }\n\n    private AutoScalingGroup mkASG(String asgName) {\n        AutoScalingGroup asg = new AutoScalingGroup().withAutoScalingGroupName(asgName);\n        // set the suspended processes\n        List<SuspendedProcess> sps = new ArrayList<SuspendedProcess>();\n        sps.add(new SuspendedProcess().withProcessName(\"Launch\")\n                .withSuspensionReason(\"User suspended at 2012-12-02T23:00:03\"));\n        sps.add(new SuspendedProcess().withProcessName(\"AddToLoadBalancer\")\n                .withSuspensionReason(\"User suspended at 2012-12-03T23:00:03\"));\n        asg.setSuspendedProcesses(sps);\n        return asg;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestEBSSnapshotJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n//CHECKSTYLE IGNORE Javadoc\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Date;\nimport java.util.EnumSet;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.amazonaws.services.ec2.model.Snapshot;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\npublic class TestEBSSnapshotJanitorCrawler {\n\n    @Test\n    public void testResourceTypes() {\n        Date startTime = new Date();\n        List<Snapshot> snapshotList = createSnapshotList(startTime);\n        EBSSnapshotJanitorCrawler crawler = new EBSSnapshotJanitorCrawler(createMockAWSClient(snapshotList));\n        EnumSet<?> types = crawler.resourceTypes();\n        Assert.assertEquals(types.size(), 1);\n        Assert.assertEquals(types.iterator().next().name(), \"EBS_SNAPSHOT\");\n    }\n\n    @Test\n    public void testSnapshotsWithNullIds() {\n        Date startTime = new Date();\n        List<Snapshot> snapshotList = createSnapshotList(startTime);\n        EBSSnapshotJanitorCrawler crawler = new EBSSnapshotJanitorCrawler(createMockAWSClient(snapshotList));\n        List<Resource> resources = crawler.resources();\n        verifySnapshotList(resources, snapshotList, startTime);\n    }\n\n    @Test\n    public void testSnapshotsWithIds() {\n        Date startTime = new Date();\n        List<Snapshot> snapshotList = createSnapshotList(startTime);\n        String[] ids = {\"snap-12345678901234567\", \"snap-12345678901234567\"};\n        EBSSnapshotJanitorCrawler crawler = new EBSSnapshotJanitorCrawler(createMockAWSClient(snapshotList, ids));\n        List<Resource> resources = crawler.resources(ids);\n        verifySnapshotList(resources, snapshotList, startTime);\n    }\n\n    @Test\n    public void testSnapshotsWithResourceType() {\n        Date startTime = new Date();\n        List<Snapshot> snapshotList = createSnapshotList(startTime);\n        EBSSnapshotJanitorCrawler crawler = new EBSSnapshotJanitorCrawler(createMockAWSClient(snapshotList));\n        for (AWSResourceType resourceType : AWSResourceType.values()) {\n            List<Resource> resources = crawler.resources(resourceType);\n            if (resourceType == AWSResourceType.EBS_SNAPSHOT) {\n                verifySnapshotList(resources, snapshotList, startTime);\n            } else {\n                Assert.assertTrue(resources.isEmpty());\n            }\n        }\n    }\n\n    private void verifySnapshotList(List<Resource> resources, List<Snapshot> snapshotList, Date startTime) {\n        Assert.assertEquals(resources.size(), snapshotList.size());\n        for (int i = 0; i < resources.size(); i++) {\n            Snapshot snapshot = snapshotList.get(i);\n            verifySnapshot(resources.get(i), snapshot.getSnapshotId(), startTime);\n        }\n    }\n\n    private void verifySnapshot(Resource snapshot, String snapshotId, Date startTime) {\n        Assert.assertEquals(snapshot.getResourceType(), AWSResourceType.EBS_SNAPSHOT);\n        Assert.assertEquals(snapshot.getId(), snapshotId);\n        Assert.assertEquals(snapshot.getRegion(), \"us-east-1\");\n        Assert.assertEquals(((AWSResource) snapshot).getAWSResourceState(), \"completed\");\n        Assert.assertEquals(snapshot.getLaunchTime(), startTime);\n    }\n\n    private AWSClient createMockAWSClient(List<Snapshot> snapshotList, String... ids) {\n        AWSClient awsMock = mock(AWSClient.class);\n        when(awsMock.describeSnapshots(ids)).thenReturn(snapshotList);\n        when(awsMock.region()).thenReturn(\"us-east-1\");\n        return awsMock;\n    }\n\n    private List<Snapshot> createSnapshotList(Date startTime) {\n        List<Snapshot> snapshotList = new LinkedList<Snapshot>();\n        snapshotList.add(mkSnapshot(\"snap-12345678901234567\", startTime));\n        snapshotList.add(mkSnapshot(\"snap-12345678901234567\", startTime));\n        return snapshotList;\n    }\n\n    private Snapshot mkSnapshot(String snapshotId, Date startTime) {\n        return new Snapshot().withSnapshotId(snapshotId).withState(\"completed\").withStartTime(startTime);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestEBSVolumeJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n//CHECKSTYLE IGNORE Javadoc\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Date;\nimport java.util.EnumSet;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.amazonaws.services.ec2.model.Volume;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\npublic class TestEBSVolumeJanitorCrawler {\n\n    @Test\n    public void testResourceTypes() {\n        Date createTime = new Date();\n        List<Volume> volumeList = createVolumeList(createTime);\n        EBSVolumeJanitorCrawler crawler = new EBSVolumeJanitorCrawler(createMockAWSClient(volumeList));\n        EnumSet<?> types = crawler.resourceTypes();\n        Assert.assertEquals(types.size(), 1);\n        Assert.assertEquals(types.iterator().next().name(), \"EBS_VOLUME\");\n    }\n\n    @Test\n    public void testVolumesWithNullIds() {\n        Date createTime = new Date();\n        List<Volume> volumeList = createVolumeList(createTime);\n        EBSVolumeJanitorCrawler crawler = new EBSVolumeJanitorCrawler(createMockAWSClient(volumeList));\n        List<Resource> resources = crawler.resources();\n        verifyVolumeList(resources, volumeList, createTime);\n    }\n\n    @Test\n    public void testVolumesWithIds() {\n        Date createTime = new Date();\n        List<Volume> volumeList = createVolumeList(createTime);\n        String[] ids = {\"vol-12345678901234567\", \"vol-12345678901234567\"};\n        EBSVolumeJanitorCrawler crawler = new EBSVolumeJanitorCrawler(createMockAWSClient(volumeList, ids));\n        List<Resource> resources = crawler.resources(ids);\n        verifyVolumeList(resources, volumeList, createTime);\n    }\n\n    @Test\n    public void testVolumesWithResourceType() {\n        Date createTime = new Date();\n        List<Volume> volumeList = createVolumeList(createTime);\n        EBSVolumeJanitorCrawler crawler = new EBSVolumeJanitorCrawler(createMockAWSClient(volumeList));\n        for (AWSResourceType resourceType : AWSResourceType.values()) {\n            List<Resource> resources = crawler.resources(resourceType);\n            if (resourceType == AWSResourceType.EBS_VOLUME) {\n                verifyVolumeList(resources, volumeList, createTime);\n            } else {\n                Assert.assertTrue(resources.isEmpty());\n            }\n        }\n    }\n\n    private void verifyVolumeList(List<Resource> resources, List<Volume> volumeList, Date createTime) {\n        Assert.assertEquals(resources.size(), volumeList.size());\n        for (int i = 0; i < resources.size(); i++) {\n            Volume volume = volumeList.get(i);\n            verifyVolume(resources.get(i), volume.getVolumeId(), createTime);\n        }\n    }\n\n    private void verifyVolume(Resource volume, String volumeId, Date createTime) {\n        Assert.assertEquals(volume.getResourceType(), AWSResourceType.EBS_VOLUME);\n        Assert.assertEquals(volume.getId(), volumeId);\n        Assert.assertEquals(volume.getRegion(), \"us-east-1\");\n        Assert.assertEquals(((AWSResource) volume).getAWSResourceState(), \"available\");\n        Assert.assertEquals(volume.getLaunchTime(), createTime);\n    }\n\n    private AWSClient createMockAWSClient(List<Volume> volumeList, String... ids) {\n        AWSClient awsMock = mock(AWSClient.class);\n        when(awsMock.describeVolumes(ids)).thenReturn(volumeList);\n        when(awsMock.region()).thenReturn(\"us-east-1\");\n        return awsMock;\n    }\n\n    private List<Volume> createVolumeList(Date createTime) {\n        List<Volume> volumeList = new LinkedList<Volume>();\n        volumeList.add(mkVolume(\"vol-12345678901234567\", createTime));\n        volumeList.add(mkVolume(\"vol-12345678901234567\", createTime));\n        return volumeList;\n    }\n\n    private Volume mkVolume(String volumeId, Date createTime) {\n        return new Volume().withVolumeId(volumeId).withState(\"available\").withCreateTime(createTime);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestELBJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.elasticloadbalancing.model.Instance;\nimport com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport org.apache.commons.lang.math.NumberUtils;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport java.util.Arrays;\nimport java.util.EnumSet;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class TestELBJanitorCrawler {\n\n    @Test\n    public void testResourceTypes() {\n        boolean includeInstances = false;\n        AWSClient client = createMockAWSClient();\n        addELBsToMock(client, createELBList(includeInstances));\n        ELBJanitorCrawler crawler = new ELBJanitorCrawler(client);\n        EnumSet<?> types = crawler.resourceTypes();\n        Assert.assertEquals(types.size(), 1);\n        Assert.assertEquals(types.iterator().next().name(), \"ELB\");\n    }\n\n    @Test\n    public void testElbsWithNoInstances() {\n        boolean includeInstances = false;\n        AWSClient client = createMockAWSClient();\n        List<LoadBalancerDescription> elbs = createELBList(includeInstances);\n        addELBsToMock(client, elbs);\n        ELBJanitorCrawler crawler = new ELBJanitorCrawler(client);\n        List<Resource> resources = crawler.resources();\n        verifyELBList(resources, elbs);\n    }\n\n    @Test\n    public void testElbsWithInstances() {\n        boolean includeInstances = true;\n        AWSClient client = createMockAWSClient();\n        List<LoadBalancerDescription> elbs = createELBList(includeInstances);\n        addELBsToMock(client, elbs);\n        ELBJanitorCrawler crawler = new ELBJanitorCrawler(client);\n        List<Resource> resources = crawler.resources();\n        verifyELBList(resources, elbs);\n    }\n\n    @Test\n    public void testElbsWithReferencedASGs() {\n        boolean includeInstances = true;\n        boolean includeELbs = true;\n        AWSClient client = createMockAWSClient();\n        List<LoadBalancerDescription> elbs = createELBList(includeInstances);\n        List<AutoScalingGroup> asgs = createASGList(includeELbs);\n        addELBsToMock(client, elbs);\n        addASGsToMock(client, asgs);\n        ELBJanitorCrawler crawler = new ELBJanitorCrawler(client);\n        List<Resource> resources = crawler.resources();\n        verifyELBList(resources, elbs, 1);\n    }\n\n    @Test\n    public void testElbsWithNoReferencedASGs() {\n        boolean includeInstances = true;\n        boolean includeELbs = false;\n        AWSClient client = createMockAWSClient();\n        List<LoadBalancerDescription> elbs = createELBList(includeInstances);\n        List<AutoScalingGroup> asgs = createASGList(includeELbs);\n        addELBsToMock(client, elbs);\n        addASGsToMock(client, asgs);\n        ELBJanitorCrawler crawler = new ELBJanitorCrawler(client);\n        List<Resource> resources = crawler.resources();\n        verifyELBList(resources, elbs, 0);\n    }\n\n    @Test\n    public void testElbsWithMultipleReferencedASGs() {\n        boolean includeInstances = true;\n        boolean includeELbs = false;\n        AWSClient client = createMockAWSClient();\n        List<LoadBalancerDescription> elbs = createELBList(includeInstances);\n        List<AutoScalingGroup> asgs = createASGList(includeELbs);\n        asgs.get(0).setLoadBalancerNames(Arrays.asList(\"elb1\", \"elb2\"));\n        addELBsToMock(client, elbs);\n        addASGsToMock(client, asgs);\n\n        ELBJanitorCrawler crawler = new ELBJanitorCrawler(client);\n        List<Resource> resources = crawler.resources();\n        verifyELBList(resources, elbs, 1);\n    }\n\n    private void verifyELBList(List<Resource> resources, List<LoadBalancerDescription> elbList) {\n        verifyELBList(resources, elbList, 0);\n    }\n\n    private void verifyELBList(List<Resource> resources, List<LoadBalancerDescription> elbList, int asgCount) {\n        Assert.assertEquals(resources.size(), elbList.size());\n        for (int i = 0; i < resources.size(); i++) {\n            LoadBalancerDescription elb = elbList.get(i);\n            verifyELB(resources.get(i), elb, asgCount);\n        }\n    }\n\n    private void verifyELB(Resource asg, LoadBalancerDescription elb, int asgCount) {\n        Assert.assertEquals(asg.getResourceType(), AWSResourceType.ELB);\n        Assert.assertEquals(asg.getId(), elb.getLoadBalancerName());\n        Assert.assertEquals(asg.getRegion(), \"us-east-1\");\n\n        int instanceCount = elb.getInstances().size();\n        int resourceInstanceCount = NumberUtils.toInt(asg.getAdditionalField(\"instanceCount\"));\n        Assert.assertEquals(instanceCount, resourceInstanceCount);\n\n        int resourceASGCount = NumberUtils.toInt(asg.getAdditionalField(\"referencedASGCount\"));\n        Assert.assertEquals(resourceASGCount, asgCount);\n    }\n\n    private AWSClient createMockAWSClient() {\n        AWSClient awsMock = mock(AWSClient.class);\n        return awsMock;\n    }\n\n    private void addELBsToMock(AWSClient awsMock, List<LoadBalancerDescription> elbList, String... elbNames) {\n        when(awsMock.describeElasticLoadBalancers(elbNames)).thenReturn(elbList);\n        when(awsMock.region()).thenReturn(\"us-east-1\");\n    }\n\n    private void addASGsToMock(AWSClient awsMock, List<AutoScalingGroup> asgList) {\n        when(awsMock.describeAutoScalingGroups()).thenReturn(asgList);\n        when(awsMock.region()).thenReturn(\"us-east-1\");\n    }\n\n    private List<LoadBalancerDescription> createELBList(boolean includeInstances) {\n        List<LoadBalancerDescription> elbList = new LinkedList<>();\n        elbList.add(mkELB(\"elb1\", includeInstances));\n        elbList.add(mkELB(\"elb2\", includeInstances));\n        return elbList;\n    }\n\n    private LoadBalancerDescription mkELB(String elbName, boolean includeInstances) {\n        LoadBalancerDescription elb = new LoadBalancerDescription().withLoadBalancerName(elbName);\n        if (includeInstances) {\n            List<Instance> instances = new LinkedList<>();\n            Instance i1 = new Instance().withInstanceId(\"i-000001\");\n            Instance i2 = new Instance().withInstanceId(\"i-000002\");\n            elb.setInstances(instances);\n        }\n        return elb;\n    }\n\n    private List<AutoScalingGroup> createASGList(boolean includeElbs) {\n        List<AutoScalingGroup> asgList = new LinkedList<AutoScalingGroup>();\n        if (includeElbs) {\n            asgList.add(mkASG(\"asg1\", \"elb1\"));\n            asgList.add(mkASG(\"asg2\", \"elb2\"));\n        } else {\n            asgList.add(mkASG(\"asg1\", null));\n            asgList.add(mkASG(\"asg2\", null));\n        }\n        return asgList;\n    }\n\n    private AutoScalingGroup mkASG(String asgName, String elb) {\n        AutoScalingGroup asg = new AutoScalingGroup().withAutoScalingGroupName(asgName);\n        asg.setLoadBalancerNames(Arrays.asList(elb));\n        return asg;\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestInstanceJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Collections;\nimport java.util.EnumSet;\nimport java.util.LinkedList;\nimport java.util.List;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingInstanceDetails;\nimport com.amazonaws.services.ec2.model.Instance;\nimport com.amazonaws.services.ec2.model.InstanceState;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\n\npublic class TestInstanceJanitorCrawler {\n\n    @Test\n    public void testResourceTypes() {\n        List<AutoScalingInstanceDetails> instanceDetailsList = createInstanceDetailsList();\n        List<Instance> instanceList = createInstanceList();\n        InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(createMockAWSClient(\n                instanceDetailsList, instanceList));\n        EnumSet<?> types = crawler.resourceTypes();\n        Assert.assertEquals(types.size(), 1);\n        Assert.assertEquals(types.iterator().next().name(), \"INSTANCE\");\n    }\n\n    @Test\n    public void testInstancesWithNullIds() {\n        List<AutoScalingInstanceDetails> instanceDetailsList = createInstanceDetailsList();\n        List<Instance> instanceList = createInstanceList();\n        AWSClient awsMock = createMockAWSClient(instanceDetailsList, instanceList);\n        InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(awsMock);\n        List<Resource> resources = crawler.resources();\n        verifyInstanceList(resources, instanceDetailsList);\n    }\n\n    @Test\n    public void testInstancesWithIds() {\n        List<AutoScalingInstanceDetails> instanceDetailsList = createInstanceDetailsList();\n        List<Instance> instanceList = createInstanceList();\n        String[] ids = {\"i-12345678901234560\", \"i-12345678901234561\"};\n        AWSClient awsMock = createMockAWSClient(instanceDetailsList, instanceList, ids);\n        InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(awsMock);\n        List<Resource> resources = crawler.resources(ids);\n        verifyInstanceList(resources, instanceDetailsList);\n    }\n\n    @Test\n    public void testInstancesWithResourceType() {\n        List<AutoScalingInstanceDetails> instanceDetailsList = createInstanceDetailsList();\n        List<Instance> instanceList = createInstanceList();\n        AWSClient awsMock = createMockAWSClient(instanceDetailsList, instanceList);\n        InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(awsMock);\n        for (AWSResourceType resourceType : AWSResourceType.values()) {\n            List<Resource> resources = crawler.resources(resourceType);\n            if (resourceType == AWSResourceType.INSTANCE) {\n                verifyInstanceList(resources, instanceDetailsList);\n            } else {\n                Assert.assertTrue(resources.isEmpty());\n            }\n        }\n    }\n\n    @Test\n    public void testInstancesNotExistingInASG() {\n        List<AutoScalingInstanceDetails> instanceDetailsList = Collections.emptyList();\n        List<Instance> instanceList = createInstanceList();\n        AWSClient awsMock = createMockAWSClient(instanceDetailsList, instanceList);\n        InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(awsMock);\n        List<Resource> resources = crawler.resources();\n        Assert.assertEquals(resources.size(), instanceList.size());\n    }\n\n    private void verifyInstanceList(List<Resource> resources, List<AutoScalingInstanceDetails> instanceList) {\n        Assert.assertEquals(resources.size(), instanceList.size());\n        for (int i = 0; i < resources.size(); i++) {\n            AutoScalingInstanceDetails instance = instanceList.get(i);\n            verifyInstance(resources.get(i), instance.getInstanceId(), instance.getAutoScalingGroupName());\n        }\n    }\n\n    private void verifyInstance(Resource instance, String instanceId, String asgName) {\n        Assert.assertEquals(instance.getResourceType(), AWSResourceType.INSTANCE);\n        Assert.assertEquals(instance.getId(), instanceId);\n        Assert.assertEquals(instance.getRegion(), \"us-east-1\");\n        Assert.assertEquals(instance.getAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME), asgName);\n        Assert.assertEquals(((AWSResource) instance).getAWSResourceState(), \"running\");\n    }\n\n    private AWSClient createMockAWSClient(List<AutoScalingInstanceDetails> instanceDetailsList,\n            List<Instance> instanceList, String... ids) {\n        AWSClient awsMock = mock(AWSClient.class);\n        when(awsMock.describeAutoScalingInstances(ids)).thenReturn(instanceDetailsList);\n        when(awsMock.describeInstances(ids)).thenReturn(instanceList);\n        when(awsMock.region()).thenReturn(\"us-east-1\");\n        return awsMock;\n    }\n\n    private List<AutoScalingInstanceDetails> createInstanceDetailsList() {\n        List<AutoScalingInstanceDetails> instanceList = new LinkedList<AutoScalingInstanceDetails>();\n        instanceList.add(mkInstanceDetails(\"i-12345678901234560\", \"asg1\"));\n        instanceList.add(mkInstanceDetails(\"i-12345678901234561\", \"asg2\"));\n        return instanceList;\n    }\n\n    private AutoScalingInstanceDetails mkInstanceDetails(String instanceId, String asgName) {\n        return new AutoScalingInstanceDetails().withInstanceId(instanceId).withAutoScalingGroupName(asgName);\n    }\n\n    private List<Instance> createInstanceList() {\n        List<Instance> instanceList = new LinkedList<Instance>();\n        instanceList.add(mkInstance(\"i-12345678901234560\"));\n        instanceList.add(mkInstance(\"i-12345678901234561\"));\n        return instanceList;\n    }\n\n    private Instance mkInstance(String instanceId) {\n        return new Instance().withInstanceId(instanceId)\n                .withState(new InstanceState().withName(\"running\"));\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestLaunchConfigJanitorCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n\npackage com.netflix.simianarmy.aws.janitor.crawler;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.LaunchConfiguration;\nimport com.google.common.collect.Lists;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport org.apache.commons.lang.Validate;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport java.util.EnumSet;\nimport java.util.List;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class TestLaunchConfigJanitorCrawler {\n\n    @Test\n    public void testResourceTypes() {\n        int n = 2;\n        String[] lcNames = {\"launchConfig1\", \"launchConfig2\"};\n        LaunchConfigJanitorCrawler crawler = new LaunchConfigJanitorCrawler(createMockAWSClient(\n                createASGList(n), createLaunchConfigList(n), lcNames));\n        EnumSet<?> types = crawler.resourceTypes();\n        Assert.assertEquals(types.size(), 1);\n        Assert.assertEquals(types.iterator().next().name(), \"LAUNCH_CONFIG\");\n    }\n\n    @Test\n    public void testInstancesWithNullNames() {\n        int n = 2;\n        List<LaunchConfiguration> lcList = createLaunchConfigList(n);\n        LaunchConfigJanitorCrawler crawler = new LaunchConfigJanitorCrawler(createMockAWSClient(\n                createASGList(n), lcList));\n        List<Resource> resources = crawler.resources();\n        verifyLaunchConfigList(resources, lcList);\n    }\n\n    @Test\n    public void testInstancesWithNames() {\n        int n = 2;\n        String[] lcNames = {\"launchConfig1\", \"launchConfig2\"};\n        List<LaunchConfiguration> lcList = createLaunchConfigList(n);\n        LaunchConfigJanitorCrawler crawler = new LaunchConfigJanitorCrawler(createMockAWSClient(\n                createASGList(n), lcList, lcNames));\n        List<Resource> resources = crawler.resources(lcNames);\n        verifyLaunchConfigList(resources, lcList);\n    }\n\n    @Test\n    public void testInstancesWithResourceType() {\n        int n = 2;\n        List<LaunchConfiguration> lcList = createLaunchConfigList(n);\n        LaunchConfigJanitorCrawler crawler = new LaunchConfigJanitorCrawler(createMockAWSClient(\n                createASGList(n), lcList));\n        for (AWSResourceType resourceType : AWSResourceType.values()) {\n            List<Resource> resources = crawler.resources(resourceType);\n            if (resourceType == AWSResourceType.LAUNCH_CONFIG) {\n                verifyLaunchConfigList(resources, lcList);\n            } else {\n                Assert.assertTrue(resources.isEmpty());\n            }\n        }\n    }\n\n    private void verifyLaunchConfigList(List<Resource> resources, List<LaunchConfiguration> lcList) {\n        Assert.assertEquals(resources.size(), lcList.size());\n        for (int i = 0; i < resources.size(); i++) {\n            LaunchConfiguration lc = lcList.get(i);\n            if (i % 2 == 0) {\n                verifyLaunchConfig(resources.get(i), lc.getLaunchConfigurationName(), true);\n            } else {\n                verifyLaunchConfig(resources.get(i), lc.getLaunchConfigurationName(), null);\n            }\n        }\n    }\n\n    private void verifyLaunchConfig(Resource launchConfig, String lcName, Boolean usedByASG) {\n        Assert.assertEquals(launchConfig.getResourceType(), AWSResourceType.LAUNCH_CONFIG);\n        Assert.assertEquals(launchConfig.getId(), lcName);\n        Assert.assertEquals(launchConfig.getRegion(), \"us-east-1\");\n        if (usedByASG != null) {\n            Assert.assertEquals(launchConfig.getAdditionalField(\n                    LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG), usedByASG.toString());\n        }\n    }\n\n    private AWSClient createMockAWSClient(List<AutoScalingGroup> asgList,\n                                          List<LaunchConfiguration> lcList, String... lcNames) {\n        AWSClient awsMock = mock(AWSClient.class);\n        when(awsMock.describeAutoScalingGroups()).thenReturn(asgList);\n        when(awsMock.describeLaunchConfigurations(lcNames)).thenReturn(lcList);\n        when(awsMock.region()).thenReturn(\"us-east-1\");\n        return awsMock;\n    }\n\n    private List<LaunchConfiguration> createLaunchConfigList(int n) {\n        List<LaunchConfiguration> lcList = Lists.newArrayList();\n        for (int i = 1; i <= n; i++) {\n            lcList.add(mkLaunchConfig(\"launchConfig\" + i));\n        }\n        return lcList;\n    }\n\n    private LaunchConfiguration mkLaunchConfig(String lcName) {\n        return new LaunchConfiguration().withLaunchConfigurationName(lcName);\n    }\n\n    private List<AutoScalingGroup> createASGList(int n) {\n        Validate.isTrue(n > 0);\n        List<AutoScalingGroup> asgList = Lists.newArrayList();\n        for (int i = 1; i <= n; i += 2) {\n            asgList.add(mkASG(\"asg\" + i, \"launchConfig\" + i));\n        }\n        return asgList;\n    }\n\n    private AutoScalingGroup mkASG(String asgName, String lcName) {\n        return new AutoScalingGroup().withAutoScalingGroupName(asgName).withLaunchConfigurationName(lcName);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/TestMonkeyCalendar.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\npackage com.netflix.simianarmy.aws.janitor.rule;\n\nimport java.util.Calendar;\nimport java.util.Date;\n\nimport org.joda.time.DateTime;\n\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyCalendar;\n\n/**\n * The class is an implementation of MonkeyCalendar that can always run and\n * considers calendar days only when calculating the termination date.\n *\n */\npublic class TestMonkeyCalendar implements MonkeyCalendar {\n    @Override\n    public boolean isMonkeyTime(Monkey monkey) {\n        return true;\n    }\n\n    @Override\n    public int openHour() {\n        return 0;\n    }\n\n    @Override\n    public int closeHour() {\n        return 24;\n    }\n\n    @Override\n    public Calendar now() {\n        return Calendar.getInstance();\n    }\n\n    @Override\n    public Date getBusinessDay(Date date, int n) {\n        DateTime target = new DateTime(date.getTime()).plusDays(n);\n        return new Date(target.getMillis());\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/asg/TestOldEmptyASGRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.aws.janitor.rule.asg;\n\nimport java.util.Date;\n\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.TestUtils;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\n\n\npublic class TestOldEmptyASGRule {\n    @Test\n    public void testEmptyASGWithObsoleteLaunchConfig() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, \"launchConfig\");\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        int launchConfiguAgeThreshold = 60;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME,\n                String.valueOf(now.minusDays(launchConfiguAgeThreshold + 1).getMillis()));\n        int retentionDays = 3;\n        OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, now);\n    }\n\n    @Test\n    public void testEmptyASGWithValidLaunchConfig() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, \"launchConfig\");\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        int launchConfiguAgeThreshold = 60;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME,\n                String.valueOf(now.minusDays(launchConfiguAgeThreshold - 1).getMillis()));\n        int retentionDays = 3;\n        OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testASGWithInstances() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, \"launchConfig\");\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"2\");\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES, \"123456789012345671,i-123456789012345672\");\n        int launchConfiguAgeThreshold = 60;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME,\n                String.valueOf(now.minusDays(launchConfiguAgeThreshold + 1).getMillis()));\n        int retentionDays = 3;\n        OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testASGWithoutInstanceAndNonZeroSize() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, \"launchConfig\");\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"2\");\n        int launchConfiguAgeThreshold = 60;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME,\n                String.valueOf(now.minusDays(launchConfiguAgeThreshold + 1).getMillis()));\n        int retentionDays = 3;\n        OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testEmptyASGWithoutLaunchConfig() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        int launchConfiguAgeThreshold = 60;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        int retentionDays = 3;\n        OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, now);\n    }\n\n    @Test\n    public void testEmptyASGWithLaunchConfigWithoutCreateTime() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, \"launchConfig\");\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        int launchConfiguAgeThreshold = 60;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int retentionDays = 3;\n        OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testResourceWithExpectedTerminationTimeSet() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        int launchConfiguAgeThreshold = 60;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        int retentionDays = 3;\n        OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Date oldTermDate = new Date(now.plusDays(10).getMillis());\n        String oldTermReason = \"Foo\";\n        resource.setExpectedTerminationTime(oldTermDate);\n        resource.setTerminationReason(oldTermReason);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime());\n        Assert.assertEquals(oldTermReason, resource.getTerminationReason());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullValidator() {\n        new OldEmptyASGRule(new TestMonkeyCalendar(), 3, 60, null);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullResource() {\n        OldEmptyASGRule rule = new OldEmptyASGRule(new TestMonkeyCalendar(), 3, 60, new DummyASGInstanceValidator());\n        rule.isValid(null);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeRetentionDays() {\n        new OldEmptyASGRule(new TestMonkeyCalendar(), -1, 60, new DummyASGInstanceValidator());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeLaunchConfigAgeThreshold() {\n        new OldEmptyASGRule(new TestMonkeyCalendar(), 3, -1, new DummyASGInstanceValidator());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullCalendar() {\n        new OldEmptyASGRule(null, 3, 60, new DummyASGInstanceValidator());\n    }\n\n    @Test\n    public void testNonASGResource() {\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE);\n        OldEmptyASGRule rule = new OldEmptyASGRule(new TestMonkeyCalendar(), 3, 60, new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/asg/TestSuspendedASGRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n//CHECKSTYLE IGNORE Javadoc\n//CHECKSTYLE IGNORE MagicNumberCheck\n\n\npackage com.netflix.simianarmy.aws.janitor.rule.asg;\n\nimport java.util.Date;\n\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.TestUtils;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\n\n\npublic class TestSuspendedASGRule {\n    @Test\n    public void testEmptyASGSuspendedMoreThanThreshold() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        int suspensionAgeThreshold = 2;\n        DateTime suspensionTime = now.minusDays(suspensionAgeThreshold + 1);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME,\n                ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.print(suspensionTime));\n        int retentionDays = 3;\n        SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, now);\n    }\n\n    @Test\n    public void testEmptyASGSuspendedLessThanThreshold() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, \"launchConfig\");\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        int suspensionAgeThreshold = 2;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME,\n                String.valueOf(now.minusDays(suspensionAgeThreshold + 1).getMillis()));\n        int retentionDays = 3;\n        SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testASGWithInstances() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"2\");\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES, \"123456789012345671,i-123456789012345672\");\n        int suspensionAgeThreshold = 2;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        DateTime suspensionTime = now.minusDays(suspensionAgeThreshold + 1);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME,\n                ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.print(suspensionTime));\n        int retentionDays = 3;\n        SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testASGWithoutInstanceAndNonZeroSize() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"2\");\n        int suspensionAgeThreshold = 2;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        DateTime suspensionTime = now.minusDays(suspensionAgeThreshold + 1);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME,\n                ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.print(suspensionTime));\n        int retentionDays = 3;\n        SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testEmptyASGNotSuspended() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        int suspensionAgeThreshold = 2;\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int retentionDays = 3;\n        SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testResourceWithExpectedTerminationTimeSet() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        int suspensionAgeThreshold = 2;\n        DateTime suspensionTime = now.minusDays(suspensionAgeThreshold + 1);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME,\n                ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.print(suspensionTime));\n        int retentionDays = 3;\n        SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Date oldTermDate = new Date(now.plusDays(10).getMillis());\n        String oldTermReason = \"Foo\";\n        resource.setExpectedTerminationTime(oldTermDate);\n        resource.setTerminationReason(oldTermReason);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime());\n        Assert.assertEquals(oldTermReason, resource.getTerminationReason());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullResource() {\n        SuspendedASGRule rule = new SuspendedASGRule(new TestMonkeyCalendar(), 3, 2, new DummyASGInstanceValidator());\n        rule.isValid(null);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullValidator() {\n        new SuspendedASGRule(new TestMonkeyCalendar(), 3, 2, null);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeRetentionDays() {\n        new SuspendedASGRule(new TestMonkeyCalendar(), -1, 2, new DummyASGInstanceValidator());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeLaunchConfigAgeThreshold() {\n        new SuspendedASGRule(new TestMonkeyCalendar(), 3, -1, new DummyASGInstanceValidator());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testSuspensionTimeIncorrectFormat() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, \"0\");\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int suspensionAgeThreshold = 2;\n        resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME, \"foo\");\n        int retentionDays = 3;\n        SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays,\n                new DummyASGInstanceValidator());\n        Assert.assertFalse(rule.isValid(resource));\n    }\n\n    @Test\n    public void testNonASGResource() {\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE);\n        SuspendedASGRule rule = new SuspendedASGRule(new TestMonkeyCalendar(), 3, 2, new DummyASGInstanceValidator());\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullCalendar() {\n        new SuspendedASGRule(null, 3, 2, null);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/elb/TestOrphanedELBRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.aws.janitor.rule.elb;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.TestUtils;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\npublic class TestOrphanedELBRule {\n\n    @Test\n    public void testELBWithNoInstancesNoASGs() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"test-elb\").withResourceType(AWSResourceType.ELB)\n                .withOwnerEmail(\"owner@foo.com\");\n        resource.setAdditionalField(\"referencedASGCount\", \"0\");\n        resource.setAdditionalField(\"instanceCount\", \"0\");\n        OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7);\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, 7, now);\n    }\n\n    @Test\n    public void testELBWithInstancesNoASGs() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"test-elb\").withResourceType(AWSResourceType.ELB)\n                .withOwnerEmail(\"owner@foo.com\");\n        resource.setAdditionalField(\"referencedASGCount\", \"0\");\n        resource.setAdditionalField(\"instanceCount\", \"4\");\n        OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7);\n        Assert.assertTrue(rule.isValid(resource));\n    }\n\n    @Test\n    public void testELBWithReferencedASGsNoInstances() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"test-elb\").withResourceType(AWSResourceType.ELB)\n                .withOwnerEmail(\"owner@foo.com\");\n        resource.setAdditionalField(\"referencedASGCount\", \"4\");\n        resource.setAdditionalField(\"instanceCount\", \"0\");\n        OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7);\n        Assert.assertTrue(rule.isValid(resource));\n    }\n\n    @Test\n    public void testMissingInstanceCountCheck() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"test-elb\").withResourceType(AWSResourceType.ELB)\n                .withOwnerEmail(\"owner@foo.com\");\n        resource.setAdditionalField(\"referencedASGCount\", \"0\");\n        OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7);\n        Assert.assertTrue(rule.isValid(resource));\n    }\n\n    @Test\n    public void testMissingReferencedASGCountCheck() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"test-elb\").withResourceType(AWSResourceType.ELB)\n                .withOwnerEmail(\"owner@foo.com\");\n        resource.setAdditionalField(\"instanceCount\", \"0\");\n        OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7);\n        Assert.assertTrue(rule.isValid(resource));\n    }\n\n    @Test\n    public void testMissingCountsCheck() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"test-elb\").withResourceType(AWSResourceType.ELB)\n                .withOwnerEmail(\"owner@foo.com\");\n        OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7);\n        Assert.assertTrue(rule.isValid(resource));\n    }\n\n    @Test\n    public void testMissingCountsCheckWithExtraFields() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"test-elb\").withResourceType(AWSResourceType.ELB)\n                .withOwnerEmail(\"owner@foo.com\");\n        resource.setAdditionalField(\"bogusField1\", \"0\");\n        resource.setAdditionalField(\"bogusField2\", \"0\");\n        OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7);\n        Assert.assertTrue(rule.isValid(resource));\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/generic/TestTagValueExclusionRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.aws.janitor.rule.generic;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport org.testng.Assert;\nimport org.testng.annotations.BeforeTest;\nimport org.testng.annotations.Test;\n\nimport java.util.HashMap;\n\npublic class TestTagValueExclusionRule {\n\n    HashMap<String,String> exclusionTags = null;\n\n    @BeforeTest\n    public void beforeTest() {\n        exclusionTags = new HashMap<>();\n        exclusionTags.put(\"tag1\", \"excludeme\");\n        exclusionTags.put(\"tag2\", \"excludeme2\");\n    }\n\n    @Test\n    public void testExcludeTaggedResourceWithTagAndValueMatch1() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n        r1.setTag(\"tag\", null);\n        r1.setTag(\"tag1\", \"excludeme\");\n        r1.setTag(\"tag2\", \"somethingelse\");\n\n        TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags);\n        Assert.assertTrue(rule.isValid(r1));\n    }\n\n    @Test\n    public void testExcludeTaggedResourceWithTagAndValueMatch2() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n        r1.setTag(\"tag\", null);\n        r1.setTag(\"tag1\", \"somethingelse\");\n        r1.setTag(\"tag2\", \"excludeme2\");\n\n        TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags);\n        Assert.assertTrue(rule.isValid(r1));\n    }\n\n    @Test\n    public void testExcludeTaggedResourceWithTagAndValueMatchBoth() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n        r1.setTag(\"tag\", null);\n        r1.setTag(\"tag1\", \"excludeme\");\n        r1.setTag(\"tag2\", \"excludeme2\");\n\n        TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags);\n        Assert.assertTrue(rule.isValid(r1));\n    }\n\n    @Test\n    public void testExcludeTaggedResourceTagMatchOnly() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n        r1.setTag(\"tag\", null);\n        r1.setTag(\"tag1\", \"somethingelse\");\n        r1.setTag(\"tag2\", \"somethingelse2\");\n\n        TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags);\n        Assert.assertFalse(rule.isValid(r1));\n    }\n\n    @Test\n    public void testExcludeTaggedResourceAllNullTags() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n        r1.setTag(\"tag\", null);\n        r1.setTag(\"tag1\", null);\n        r1.setTag(\"tag2\", null);\n\n        TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags);\n        Assert.assertFalse(rule.isValid(r1));\n    }\n\n    @Test\n    public void testExcludeTaggedResourceValueMatchOnly() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n        r1.setTag(\"tag\", null);\n        r1.setTag(\"tagA\", \"excludeme\");\n        r1.setTag(\"tagB\", \"excludeme2\");\n\n        TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags);\n        Assert.assertFalse(rule.isValid(r1));\n    }\n\n    @Test\n    public void testExcludeUntaggedResource() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n\n        TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags);\n        Assert.assertFalse(rule.isValid(r1));\n    }\n\n    @Test\n    public void testNameValueConstructor() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n        r1.setTag(\"tag1\", \"excludeme\");\n\n        String names = \"tag1\";\n        String vals  = \"excludeme\";\n        TagValueExclusionRule rule = new TagValueExclusionRule(names.split(\",\"), vals.split(\",\"));\n        Assert.assertTrue(rule.isValid(r1));\n    }\n\n    @Test\n    public void testNameValueConstructor2() {\n        Resource r1 = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail(\"owner@foo.com\");\n        r1.setTag(\"tag1\", \"excludeme\");\n\n        String names = \"tag1,tag2\";\n        String vals  = \"excludeme,excludeme2\";\n        TagValueExclusionRule rule = new TagValueExclusionRule(names.split(\",\"), vals.split(\",\"));\n        Assert.assertTrue(rule.isValid(r1));\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/generic/TestUntaggedRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.aws.janitor.rule.generic;\n\nimport java.util.Date;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.TestUtils;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\n\npublic class TestUntaggedRule {\n\n    @Test\n    public void testUntaggedInstanceWithOwner() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE)\n                .withOwnerEmail(\"owner@foo.com\");\n        resource.setTag(\"tag1\", \"value1\");\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        Set<String> tags = new HashSet<String>();\n        tags.add(\"tag1\");\n        tags.add(\"tag2\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDaysWithOwner, now);\n    }\n\n    @Test\n    public void testUntaggedInstanceWithoutOwner() {\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE);\n        resource.setTag(\"tag1\", \"value1\");\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        Set<String> tags = new HashSet<String>();\n        tags.add(\"tag1\");\n        tags.add(\"tag2\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDaysWithoutOwner, now);\n    }\n\n    @Test\n    public void testTaggedInstance() {\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE);\n        resource.setTag(\"tag1\", \"value1\");\n        resource.setTag(\"tag2\", \"value2\");\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        Set<String> tags = new HashSet<String>();\n        tags.add(\"tag1\");\n        tags.add(\"tag2\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertTrue(rule.isValid(resource));\n    }\n\n    @Test\n    public void testUntaggedResource() {\n        DateTime now = DateTime.now();\n        Resource imageResource = new AWSResource().withId(\"ami-123123\").withResourceType(AWSResourceType.IMAGE);\n        Resource asgResource = new AWSResource().withId(\"my-cool-asg\").withResourceType(AWSResourceType.ASG);\n        Resource ebsSnapshotResource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT);\n        Resource lauchConfigurationResource = new AWSResource().withId(\"my-cool-launch-configuration\").withResourceType(AWSResourceType.LAUNCH_CONFIG);\n        Set<String> tags = new HashSet<String>();\n        tags.add(\"tag1\");\n        tags.add(\"tag2\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertFalse(rule.isValid(imageResource));\n        Assert.assertFalse(rule.isValid(asgResource));\n        Assert.assertFalse(rule.isValid(ebsSnapshotResource));\n        Assert.assertFalse(rule.isValid(lauchConfigurationResource));\n        TestUtils.verifyTerminationTimeRough(imageResource, retentionDaysWithoutOwner, now);\n        TestUtils.verifyTerminationTimeRough(asgResource, retentionDaysWithoutOwner, now);\n        TestUtils.verifyTerminationTimeRough(ebsSnapshotResource, retentionDaysWithoutOwner, now);\n        TestUtils.verifyTerminationTimeRough(lauchConfigurationResource, retentionDaysWithoutOwner, now);\n    }\n\n    @Test\n    public void testResourceWithExpectedTerminationTimeSet() {\n        DateTime now = DateTime.now();\n        Date oldTermDate = new Date(now.plusDays(10).getMillis());\n        String oldTermReason = \"Foo\";\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE)\n                .withExpectedTerminationTime(oldTermDate)\n                .withTerminationReason(oldTermReason);\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        Set<String> tags = new HashSet<String>();\n        tags.add(\"tag1\");\n        tags.add(\"tag2\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime());\n        Assert.assertEquals(oldTermReason, resource.getTerminationReason());\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/instance/TestOrphanedInstanceRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.aws.janitor.rule.instance;\n\nimport java.util.Date;\n\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.TestUtils;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\n\npublic class TestOrphanedInstanceRule {\n\n    @Test\n    public void testOrphanedInstancesWithOwner() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()))\n                .withOwnerEmail(\"owner@foo.com\");\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDaysWithOwner, now);\n    }\n\n    @Test\n    public void testOrphanedInstancesWithoutOwner() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDaysWithoutOwner, now);\n    }\n\n    @Test\n    public void testOrphanedInstancesWithoutLaunchTime() {\n        int ageThreshold = 5;\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE);\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testOrphanedInstancesWithLaunchTimeNotExpires() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold - 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testNonOrphanedInstances() {\n        int ageThreshold = 5;\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE)\n                .setAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME, \"asg1\");\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testResourceWithExpectedTerminationTimeSet() {\n        DateTime now = DateTime.now();\n        Date oldTermDate = new Date(now.plusDays(10).getMillis());\n        String oldTermReason = \"Foo\";\n        int ageThreshold = 5;\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()))\n                .withExpectedTerminationTime(oldTermDate)\n                .withTerminationReason(oldTermReason);\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        int retentionDaysWithOwner = 4;\n        int retentionDaysWithoutOwner = 8;\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime());\n        Assert.assertEquals(oldTermReason, resource.getTerminationReason());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullResource() {\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), 5, 4, 8);\n        rule.isValid(null);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeAgeThreshold() {\n        new OrphanedInstanceRule(new TestMonkeyCalendar(), -1, 4, 8);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeRetentionDaysWithOwner() {\n        new OrphanedInstanceRule(new TestMonkeyCalendar(), 5, -4, 8);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeRetentionDaysWithoutOwner() {\n        new OrphanedInstanceRule(new TestMonkeyCalendar(), 5, 4, -8);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullCalendar() {\n        new OrphanedInstanceRule(null, 5, 4, 8);\n    }\n\n    @Test\n    public void testNonInstanceResource() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        ((AWSResource) resource).setAWSResourceState(\"running\");\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), 0, 0, 0);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testNonRunningInstance() {\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE);\n        ((AWSResource) resource).setAWSResourceState(\"stopping\");\n        OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), 0, 0, 0);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/launchconfig/TestOldUnusedLaunchConfigRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.aws.janitor.rule.launchconfig;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.TestUtils;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.crawler.LaunchConfigJanitorCrawler;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport java.util.Date;\n\n\npublic class TestOldUnusedLaunchConfigRule {\n\n    @Test\n    public void testOldUnsedLaunchConfig() {\n        Resource resource = new AWSResource().withId(\"launchConfig1\").withResourceType(AWSResourceType.LAUNCH_CONFIG);\n        resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, \"false\");\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int ageThreshold = 3;\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        int retentionDays = 3;\n        OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays);\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, now);\n    }\n\n    @Test\n    public void testOldLaunchConfigWithNullFlag() {\n        Resource resource = new AWSResource().withId(\"launchConfig1\").withResourceType(AWSResourceType.LAUNCH_CONFIG);\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int ageThreshold = 3;\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        int retentionDays = 3;\n        OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testUnsedLaunchConfigNotOld() {\n        Resource resource = new AWSResource().withId(\"launchConfig1\").withResourceType(AWSResourceType.LAUNCH_CONFIG);\n        resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, \"false\");\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int ageThreshold = 3;\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setLaunchTime(new Date(now.minusDays(ageThreshold - 1).getMillis()));\n        int retentionDays = 3;\n        OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testUsedLaunchConfig() {\n        Resource resource = new AWSResource().withId(\"launchConfig1\").withResourceType(AWSResourceType.LAUNCH_CONFIG);\n        resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, \"true\");\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int ageThreshold = 3;\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        int retentionDays = 3;\n        OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testUsedLaunchConfigNoLaunchTimeSet() {\n        Resource resource = new AWSResource().withId(\"launchConfig1\").withResourceType(AWSResourceType.LAUNCH_CONFIG);\n        resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, \"true\");\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int ageThreshold = 3;\n        int retentionDays = 3;\n        OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testResourceWithExpectedTerminationTimeSet() {\n        Resource resource = new AWSResource().withId(\"launchConfig1\").withResourceType(AWSResourceType.LAUNCH_CONFIG);\n        resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, \"false\");\n        MonkeyCalendar calendar = new TestMonkeyCalendar();\n        int ageThreshold = 3;\n        DateTime now = new DateTime(calendar.now().getTimeInMillis());\n        resource.setLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        Date oldTermDate = new Date(now.minusDays(10).getMillis());\n        String oldTermReason = \"Foo\";\n        resource.setExpectedTerminationTime(oldTermDate);\n        resource.setTerminationReason(oldTermReason);\n        int retentionDays = 3;\n        OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime());\n        Assert.assertEquals(oldTermReason, resource.getTerminationReason());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullResource() {\n        OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(new TestMonkeyCalendar(), 3, 3);\n        rule.isValid(null);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeRetentionDays() {\n        new OldUnusedLaunchConfigRule(new TestMonkeyCalendar(), -1, 60);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeLaunchConfigAgeThreshold() {\n        new OldUnusedLaunchConfigRule(new TestMonkeyCalendar(), 3, -1);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullCalendar() {\n        new OldUnusedLaunchConfigRule(null, 3, 60);\n    }\n\n    @Test\n    public void testNonLaunchConfigResource() {\n        Resource resource = new AWSResource().withId(\"i-12345678901234567\").withResourceType(AWSResourceType.INSTANCE);\n        OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(new TestMonkeyCalendar(), 3, 60);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/snapshot/TestNoGeneratedAMIRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n//CHECKSTYLE IGNORE Javadoc\n//CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.aws.janitor.rule.snapshot;\n\nimport java.util.Date;\n\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.TestUtils;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.crawler.EBSSnapshotJanitorCrawler;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\n\n\npublic class TestNoGeneratedAMIRule {\n\n    @Test\n    public void testNonSnapshotResource() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), 0, 0);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testUncompletedVolume() {\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT);\n        ((AWSResource) resource).setAWSResourceState(\"stopped\");\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), 0, 0);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testTaggedAsNotMark() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        int retentionDays = 4;\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        resource.setTag(JanitorMonkey.JANITOR_TAG, \"donotmark\");\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testUserSpecifiedTerminationDate() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        int retentionDays = 4;\n        DateTime userDate = new DateTime(now.plusDays(3).withTimeAtStartOfDay());\n        resource.setTag(JanitorMonkey.JANITOR_TAG,\n                NoGeneratedAMIRule.TERMINATION_DATE_FORMATTER.print(userDate));\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(resource.getExpectedTerminationTime().getTime(), userDate.getMillis());\n    }\n\n    @Test\n    public void testOldSnapshotWithoutAMI() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        int retentionDays = 4;\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, now);\n    }\n\n    @Test\n    public void testSnapshotWithoutAMINotOld() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold - 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        int retentionDays = 4;\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testWithAMIs() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        resource.setAdditionalField(EBSSnapshotJanitorCrawler.SNAPSHOT_FIELD_AMIS, \"ami-123\");\n        int retentionDays = 4;\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testSnapshotsWithoutLauchTime() {\n        int ageThreshold = 5;\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT);\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        int retentionDays = 4;\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n\n    @Test\n    public void testResourceWithExpectedTerminationTimeSet() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        Date oldTermDate = new Date(now.plusDays(10).getMillis());\n        String oldTermReason = \"Foo\";\n        int retentionDays = 4;\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        resource.setExpectedTerminationTime(oldTermDate);\n        resource.setTerminationReason(oldTermReason);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime());\n        Assert.assertEquals(oldTermReason, resource.getTerminationReason());\n    }\n\n    @Test\n    public void testOldSnapshotWithoutAMIWithOwnerOverride() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withOwnerEmail(\"owner@netflix.com\").withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        int retentionDays = 4;\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays, \"new_owner@netflix.com\");\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(resource.getOwnerEmail(), \"new_owner@netflix.com\");\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, now);\n    }\n\n    @Test\n    public void testOldSnapshotWithoutAMIWithoutOwnerOverride() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"snap-12345678901234567\").withOwnerEmail(\"owner@netflix.com\").withResourceType(AWSResourceType.EBS_SNAPSHOT)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"completed\");\n        int retentionDays = 4;\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(resource.getOwnerEmail(), \"owner@netflix.com\");\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, now);\n    }\n    \n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullResource() {\n        NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), 5, 4);\n        rule.isValid(null);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeAgeThreshold() {\n        new NoGeneratedAMIRule(new TestMonkeyCalendar(), -1, 4);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeRetentionDaysWithOwner() {\n        new NoGeneratedAMIRule(new TestMonkeyCalendar(), 5, -4);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullCalendar() {\n        new NoGeneratedAMIRule(null, 5, 4);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/aws/janitor/rule/volume/TestOldDetachedVolumeRule.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n//CHECKSTYLE IGNORE Javadoc\n//CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.aws.janitor.rule.volume;\n\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.TimeZone;\n\nimport org.joda.time.DateTime;\nimport org.joda.time.DateTimeZone;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.MonkeyCalendar;\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.TestUtils;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.AWSResourceType;\nimport com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\nimport com.netflix.simianarmy.janitor.JanitorMonkey;\n\nimport static org.joda.time.DateTimeConstants.MILLIS_PER_DAY;\n\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.when;\n\n\npublic class TestOldDetachedVolumeRule {\n\n    @Test\n    public void testNonVolumeResource() {\n        Resource resource = new AWSResource().withId(\"asg1\").withResourceType(AWSResourceType.ASG);\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), 0, 0);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testUnavailableVolume() {\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME);\n        ((AWSResource) resource).setAWSResourceState(\"stopped\");\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), 0, 0);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testTaggedAsNotMark() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        Date lastDetachTime = new Date(now.minusDays(ageThreshold + 1).getMillis());\n        String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime);\n        resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag);\n        int retentionDays = 4;\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        resource.setTag(JanitorMonkey.JANITOR_TAG, \"donotmark\");\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testNoMetaTag() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        int retentionDays = 4;\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        resource.setTag(JanitorMonkey.JANITOR_TAG, \"donotmark\");\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testUserSpecifiedTerminationDate() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        int retentionDays = 4;\n        DateTime userDate = new DateTime(now.plusDays(3).withTimeAtStartOfDay());\n        resource.setTag(JanitorMonkey.JANITOR_TAG,\n                OldDetachedVolumeRule.TERMINATION_DATE_FORMATTER.print(userDate));\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(resource.getExpectedTerminationTime().getTime(), userDate.getMillis());\n    }\n\n    @Test\n    public void testOldDetachedVolume() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        Date lastDetachTime = new Date(now.minusDays(ageThreshold + 1).getMillis());\n        String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime);\n        resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag);\n        int retentionDays = 4;\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertFalse(rule.isValid(resource));\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, now);\n    }\n\n    /** This test exists to check logic on a utility method.\n     * The tagging rule for resource expiry uses a variable nubmer of days.\n     * However, JodaTime date arithmetic for DAYS uses calendar days.  It does NOT\n     * treat a day as 24 hours in this case (HOUR arithmetic, however, does).\n     * Therefore, a termination policy of 4 days (96 hours) will actually occur in\n     * 95 hours if the resource is tagged with that rule within 4 days of the DST\n     * cutover.\n     *\n     * We experienced test case failures around the 2014 spring DST cutover that\n     * prevented us from getting green builds.  So, the assertion logic was loosened\n     * to check that we were within a day of the expected date.  For the test case,\n     * all but 4 days of the year this problem never shows up.  To verify that our\n     * fix was correct, this test case explicitly sets the date.  The other tests\n     * that use a DateTime of \"DateTime.now()\" are not true unit tests, because the\n     * test does not isolate the date.  They are actually a partial integration test,\n     * as they leave the date up to the system where the test executes.\n     *\n     * We have to mock the call to MonkeyCalendar.now() because the constructor\n     * for that class uses Calendar.getInstance() internally.\n     *\n     */\n    @Test\n    public void testOldDetachedVolumeBeforeDaylightSavingsCutover() {\n        int ageThreshold = 5;\n        //here we set the create date to a few days before a known DST cutover, where\n        //we observed DST failures\n        DateTime closeToSpringAheadDst = new DateTime(2014, 3, 7, 0, 0, DateTimeZone.forID(\"America/Los_Angeles\"));\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME)\n            .withLaunchTime(new Date(closeToSpringAheadDst.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        Date lastDetachTime = new Date(closeToSpringAheadDst.minusDays(ageThreshold + 1).getMillis());\n        String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime);\n        resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag);\n        int retentionDays = 4;\n\n        //set the \"now\" to the fixed execution date for this rule and create a partial mock\n        Calendar fixed = Calendar.getInstance(TimeZone.getTimeZone(\"America/Los_Angeles\"));\n        fixed.setTimeInMillis(closeToSpringAheadDst.getMillis());\n        MonkeyCalendar monkeyCalendar = new TestMonkeyCalendar();\n        MonkeyCalendar spyCalendar = spy(monkeyCalendar);\n        when(spyCalendar.now()).thenReturn(fixed);\n\n        //use the partial mock for the OldDetachedVolumeRule\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(spyCalendar, ageThreshold, retentionDays);\n        Assert.assertFalse(rule.isValid(resource)); //this volume should be seen as invalid\n\n        //3.26.2014. Commenting out DST cutover verification.  A line in\n        //OldDetachedVolumeRule.isValid actually creates a new date from the Tag value.\n        //while this unit test tries its best to use invariants, the tag does not contain timezone\n        //information, so a time set in the Los Angeles timezone and tagged, then parsed as\n        //UTC (if that's how the VM running the test is set) will fail.\n\n\n/////////////////////////////\n        //Leaving the code in place to be uncommnented later if that class is refactored\n        //to support a design that promotes more complete testing.\n\n        //now verify that the difference between \"now\" and the cutoff is slightly under the intended\n        //retention limit, as the DST cutover makes us lose one hour\n        //verifyDSTCutoverHappened(resource, retentionDays, closeToSpringAheadDst);\n/////////////////////////////\n        //now verify that our projected termination time is within one day of what was asked for\n        TestUtils.verifyTerminationTimeRough(resource, retentionDays, closeToSpringAheadDst);\n    }\n\n    @Test\n    public void testDetachedVolumeNotOld() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        Date lastDetachTime = new Date(now.minusDays(ageThreshold - 1).getMillis());\n        String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime);\n        resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag);\n        int retentionDays = 4;\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n    @Test\n    public void testAttachedVolume() {\n        int ageThreshold = 5;\n        DateTime now = DateTime.now();\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        String metaTag = VolumeTaggingMonkey.makeMetaTag(\"i-12345678901234567\", \"owner\", null);\n        resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag);\n        int retentionDays = 4;\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        Assert.assertTrue(rule.isValid(resource));\n        Assert.assertNull(resource.getExpectedTerminationTime());\n    }\n\n\n    @Test\n    public void testResourceWithExpectedTerminationTimeSet() {\n        DateTime now = DateTime.now();\n        Date oldTermDate = new Date(now.plusDays(10).getMillis());\n        String oldTermReason = \"Foo\";\n        int ageThreshold = 5;\n        Resource resource = new AWSResource().withId(\"vol-12345678901234567\").withResourceType(AWSResourceType.EBS_VOLUME)\n                .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis()));\n        ((AWSResource) resource).setAWSResourceState(\"available\");\n        Date lastDetachTime = new Date(now.minusDays(ageThreshold + 1).getMillis());\n        String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime);\n        resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag);\n        int retentionDays = 4;\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(),\n                ageThreshold, retentionDays);\n        resource.setExpectedTerminationTime(oldTermDate);\n        resource.setTerminationReason(oldTermReason);\n        Assert.assertFalse(rule.isValid(resource));\n        Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime());\n        Assert.assertEquals(oldTermReason, resource.getTerminationReason());\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullResource() {\n        OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), 5, 4);\n        rule.isValid(null);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeAgeThreshold() {\n        new OldDetachedVolumeRule(new TestMonkeyCalendar(), -1, 4);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNgativeRetentionDaysWithOwner() {\n        new OldDetachedVolumeRule(new TestMonkeyCalendar(), 5, -4);\n    }\n\n    @Test(expectedExceptions = IllegalArgumentException.class)\n    public void testNullCalendar() {\n        new OldDetachedVolumeRule(null, 5, 4);\n    }\n\n    /** Verify that a test conditioned to run across the spring DST cutover actually did\n     * cross that threshold.  The real difference will be about 0.05 days less than\n     * the retentionDays parameter.\n     * @param resource The AWS resource being tested\n     * @param retentionDays Number of days the resource should be kept around\n     * @param timeOfCheck When the check is executed\n     */\n    private void verifyDSTCutoverHappened(Resource resource, int retentionDays, DateTime timeOfCheck) {\n        double realDays = (double) (resource.getExpectedTerminationTime().getTime() - timeOfCheck.getMillis())\n            / (double) MILLIS_PER_DAY;\n        long days = (resource.getExpectedTerminationTime().getTime() - timeOfCheck.getMillis()) / MILLIS_PER_DAY;\n        Assert.assertTrue(realDays < (double) retentionDays);\n        Assert.assertNotEquals(days, retentionDays);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/TestBasicCalendar.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic;\n\nimport java.util.Calendar;\nimport java.util.Properties;\nimport java.util.TimeZone;\n\nimport org.testng.Assert;\nimport org.testng.annotations.DataProvider;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.TestMonkey;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npublic class TestBasicCalendar extends BasicCalendar {\n    private static final Properties PROPS = new Properties();\n    private static final BasicConfiguration CFG = new BasicConfiguration(PROPS);\n\n    public TestBasicCalendar() {\n        super(CFG);\n    }\n\n    @Test\n    public void testConstructors() {\n        BasicCalendar cal = new BasicCalendar(CFG);\n        Assert.assertEquals(cal.openHour(), 9);\n        Assert.assertEquals(cal.closeHour(), 15);\n        Assert.assertEquals(cal.now().getTimeZone(), TimeZone.getTimeZone(\"America/Los_Angeles\"));\n\n        cal = new BasicCalendar(11, 12, TimeZone.getTimeZone(\"Europe/Stockholm\"));\n        Assert.assertEquals(cal.openHour(), 11);\n        Assert.assertEquals(cal.closeHour(), 12);\n        Assert.assertEquals(cal.now().getTimeZone(), TimeZone.getTimeZone(\"Europe/Stockholm\"));\n    }\n\n    private Calendar now = super.now();\n\n    @Override\n    public Calendar now() {\n        return (Calendar) now.clone();\n    }\n\n    private void setNow(Calendar now) {\n        this.now = now;\n    }\n\n    @Test\n    void testMonkeyTime() {\n        Calendar test = Calendar.getInstance();\n        Monkey monkey = new TestMonkey();\n\n        // using leap day b/c it is not a holiday & not a weekend\n        test.set(Calendar.YEAR, 2012);\n        test.set(Calendar.MONTH, Calendar.FEBRUARY);\n        test.set(Calendar.DAY_OF_MONTH, 29);\n        test.set(Calendar.HOUR_OF_DAY, 8); // 8am leap day\n        setNow(test);\n\n        Assert.assertFalse(isMonkeyTime(monkey));\n\n        test.set(Calendar.HOUR_OF_DAY, 10); // 10am leap day\n        setNow(test);\n\n        Assert.assertTrue(isMonkeyTime(monkey));\n\n        test.set(Calendar.HOUR_OF_DAY, 17); // 5pm leap day\n        setNow(test);\n\n        Assert.assertFalse(isMonkeyTime(monkey));\n\n        // set to the following Saturday so we can test we dont run on weekends\n        // even though within \"business hours\"\n        test.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);\n        test.set(Calendar.HOUR_OF_DAY, 10);\n        setNow(test);\n        Assert.assertFalse(isMonkeyTime(monkey));\n\n        // test config overrides\n        PROPS.setProperty(\"simianarmy.calendar.isMonkeyTime\", Boolean.toString(true));\n        Assert.assertTrue(isMonkeyTime(monkey));\n\n        PROPS.setProperty(\"simianarmy.calendar.isMonkeyTime\", Boolean.toString(false));\n        Assert.assertFalse(isMonkeyTime(monkey));\n    }\n\n    @DataProvider\n    public Object[][] holidayDataProvider() {\n        return new Object[][] {{Calendar.JANUARY, 2}, // New Year's Day\n                {Calendar.JANUARY, 16}, // MLK day\n                {Calendar.FEBRUARY, 20}, // Washington's Birthday\n                {Calendar.MAY, 28}, // Memorial Day\n                {Calendar.JULY, 4}, // Independence Day\n                {Calendar.SEPTEMBER, 3}, // Labor Day\n                {Calendar.OCTOBER, 8}, // Columbus Day\n                {Calendar.NOVEMBER, 12}, // Veterans Day\n                {Calendar.NOVEMBER, 22}, // Thanksgiving Day\n                {Calendar.DECEMBER, 25} // Christmas Day\n        };\n    }\n\n    @Test(dataProvider = \"holidayDataProvider\")\n    public void testHolidays(int month, int dayOfMonth) {\n        Calendar test = Calendar.getInstance();\n        test.set(Calendar.YEAR, 2012);\n        test.set(Calendar.MONTH, month);\n        test.set(Calendar.DAY_OF_MONTH, dayOfMonth);\n        test.set(Calendar.HOUR_OF_DAY, 10);\n        setNow(test);\n\n        Assert.assertTrue(isHoliday(test), test.getTime().toString() + \" is a holiday?\");\n    }\n\n    @Test\n    public void testGetBusinessDayWihoutGap() {\n        // the days from 12/3/2012 to 12/7/2012 are all business days\n        int hour = 10;\n        Calendar test = now();\n        test.set(Calendar.YEAR, 2012);\n        test.set(Calendar.MONTH, Calendar.DECEMBER);\n        test.set(Calendar.DAY_OF_MONTH, 3);\n        test.set(Calendar.HOUR_OF_DAY, hour);\n        int day = test.get(Calendar.DAY_OF_MONTH);\n        for (int n = 0; n <= 4; n++) {\n            Calendar businessDay = now();\n            businessDay.setTime(getBusinessDay(test.getTime(), n));\n            Assert.assertEquals(businessDay.get(Calendar.DAY_OF_MONTH),\n                    day + n);\n            Assert.assertEquals(businessDay.get(Calendar.HOUR_OF_DAY),\n                    hour);\n        }\n    }\n\n    @Test\n    public void testGetBusinessDayWihWeekend() {\n        // 12/7/2012 is Friday\n        int hour = 10;\n        Calendar test = now();\n        test.set(Calendar.YEAR, 2012);\n        test.set(Calendar.MONTH, Calendar.DECEMBER);\n        test.set(Calendar.DAY_OF_MONTH, 7);\n        test.set(Calendar.HOUR_OF_DAY, hour);\n        int day = test.get(Calendar.DAY_OF_MONTH);\n        for (int n = 1; n <= 5; n++) {\n            Calendar businessDay = now();\n            businessDay.setTime(getBusinessDay(test.getTime(), n));\n            Assert.assertEquals(businessDay.get(Calendar.DAY_OF_MONTH),\n                    day + n + 2);\n            Assert.assertEquals(businessDay.get(Calendar.HOUR_OF_DAY),\n                    hour);\n        }\n    }\n\n    @Test\n    public void testGetBusinessDayWihHoliday() {\n        // 12/23/2012 is Monday and 12/24 - 12/26 are holidays\n        int hour = 10;\n        Calendar test = now();\n        test.set(Calendar.YEAR, 2012);\n        test.set(Calendar.MONTH, Calendar.DECEMBER);\n        test.set(Calendar.DAY_OF_MONTH, 24);\n        test.set(Calendar.HOUR_OF_DAY, hour);\n        int day = test.get(Calendar.DAY_OF_MONTH);\n\n        Calendar businessDay = now();\n        businessDay.setTime(getBusinessDay(test.getTime(), 1));\n        Assert.assertEquals(businessDay.get(Calendar.DAY_OF_MONTH),\n                day + 4);\n        Assert.assertEquals(businessDay.get(Calendar.HOUR_OF_DAY),\n                hour);\n    }\n\n    @Test\n    public void testGetBusinessDayWihHolidayNextYear() {\n        // 12/28/2012 is Friday and 12/31 - 1/1 are holidays\n        int hour = 10;\n        Calendar test = now();\n        test.set(Calendar.YEAR, 2012);\n        test.set(Calendar.MONTH, Calendar.DECEMBER);\n        test.set(Calendar.DAY_OF_MONTH, 28);\n        test.set(Calendar.HOUR_OF_DAY, hour);\n\n        Calendar businessDay = now();\n        businessDay.setTime(getBusinessDay(test.getTime(), 1));\n        // The next business day should be 1/2/2013\n        Assert.assertEquals(businessDay.get(Calendar.YEAR), 2013);\n        Assert.assertEquals(businessDay.get(Calendar.MONTH), Calendar.JANUARY);\n        Assert.assertEquals(businessDay.get(Calendar.DAY_OF_MONTH), 2);\n        Assert.assertEquals(businessDay.get(Calendar.HOUR_OF_DAY), hour);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/TestBasicConfiguration.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic;\n\nimport org.testng.annotations.Test;\nimport org.testng.Assert;\n\nimport java.util.Properties;\n\npublic class TestBasicConfiguration extends BasicConfiguration {\n    private static final Properties PROPS = new Properties();\n\n    public TestBasicConfiguration() {\n        super(PROPS);\n    }\n\n    @Test\n    public void testGetBool() {\n        PROPS.clear();\n        Assert.assertFalse(getBool(\"foobar.enabled\"));\n\n        PROPS.setProperty(\"foobar.enabled\", \"true\");\n        Assert.assertTrue(getBool(\"foobar.enabled\"));\n\n        PROPS.setProperty(\"foobar.enabled\", \"false\");\n        Assert.assertFalse(getBool(\"foobar.enabled\"));\n    }\n\n    @Test\n    public void testGetBoolOrElse() {\n        PROPS.clear();\n        Assert.assertFalse(getBoolOrElse(\"foobar.enabled\", false));\n        Assert.assertTrue(getBoolOrElse(\"foobar.enabled\", true));\n\n        PROPS.setProperty(\"foobar.enabled\", \"true\");\n        Assert.assertTrue(getBoolOrElse(\"foobar.enabled\", false));\n        Assert.assertTrue(getBoolOrElse(\"foobar.enabled\", true));\n\n        PROPS.setProperty(\"foobar.enabled\", \"false\");\n        Assert.assertFalse(getBoolOrElse(\"foobar.enabled\", false));\n        Assert.assertFalse(getBoolOrElse(\"foobar.enabled\", true));\n    }\n\n    @Test\n    public void testGetNumOrElse() {\n        // CHECKSTYLE IGNORE MagicNumberCheck\n        PROPS.clear();\n        Assert.assertEquals(getNumOrElse(\"foobar.number\", 42), 42D);\n\n        PROPS.setProperty(\"foobar.number\", \"0\");\n        Assert.assertEquals(getNumOrElse(\"foobar.number\", 42), 0D);\n    }\n\n    @Test\n    public void testGetStr() {\n        PROPS.clear();\n        Assert.assertNull(getStr(\"foobar\"));\n\n        PROPS.setProperty(\"foobar\", \"string\");\n        Assert.assertEquals(getStr(\"foobar\"), \"string\");\n    }\n\n    @Test\n    public void testGetStrOrElse() {\n        PROPS.clear();\n        Assert.assertEquals(getStrOrElse(\"foobar\", \"default\"), \"default\");\n\n        PROPS.setProperty(\"foobar\", \"string\");\n        Assert.assertEquals(getStrOrElse(\"foobar\", \"default\"), \"string\");\n\n        PROPS.setProperty(\"foobar\", \"\");\n        Assert.assertEquals(getStrOrElse(\"foobar\", \"default\"), \"\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/TestBasicContext.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic;\n\nimport com.amazonaws.ClientConfiguration;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\npublic class TestBasicContext {\n    @Test\n    public void testContext() {\n        BasicChaosMonkeyContext ctx = new BasicChaosMonkeyContext();\n        Assert.assertNotNull(ctx.scheduler());\n        Assert.assertNotNull(ctx.calendar());\n        Assert.assertNotNull(ctx.configuration());\n        Assert.assertNotNull(ctx.cloudClient());\n        Assert.assertNotNull(ctx.chaosCrawler());\n        Assert.assertNotNull(ctx.chaosInstanceSelector());\n\n        Assert.assertTrue(ctx.configuration().getBool(\"simianarmy.calendar.isMonkeyTime\"));\n        Assert.assertEquals(ctx.configuration().getStr(\"simianarmy.client.aws.assumeRoleArn\"),\n                                                        \"arn:aws:iam::fakeAccount:role/fakeRole\");\n        // Verify that the property in chaos.properties overrides the same property in simianarmy.properties\n        Assert.assertFalse(ctx.configuration().getBool(\"simianarmy.chaos.enabled\"));\n    }\n\n    @Test\n    public void testIsSafeToLogProperty() {\n        BasicChaosMonkeyContext ctx = new BasicChaosMonkeyContext();\n        Assert.assertTrue(ctx.isSafeToLog(\"simianarmy.client.aws.region\"));\n    }\n\n    @Test\n    public void testIsNotSafeToLogProperty() {\n        BasicChaosMonkeyContext ctx = new BasicChaosMonkeyContext();\n        Assert.assertFalse(ctx.isSafeToLog(\"simianarmy.client.aws.secretKey\"));\n    }\n\n    @Test\n    public void testIsNotSafeToLogVsphereProperty() {\n        BasicChaosMonkeyContext ctx = new BasicChaosMonkeyContext();\n        Assert.assertFalse(ctx.isSafeToLog(\"simianarmy.client.vsphere.password\"));\n    }\n\n    @Test\n    public void testIsNotUsingProxyByDefault() {\n        BasicSimianArmyContext ctx = new BasicSimianArmyContext();\n\n        ClientConfiguration awsClientConfig = ctx.getAwsClientConfig();\n\n        Assert.assertNull(awsClientConfig.getProxyHost());\n        Assert.assertEquals(awsClientConfig.getProxyPort(), -1);\n        Assert.assertNull(awsClientConfig.getProxyUsername());\n        Assert.assertNull(awsClientConfig.getProxyPassword());\n    }\n\n        @Test\n    public void testIsAbleToUseProxyByConfiguration() {\n        BasicSimianArmyContext ctx = new BasicSimianArmyContext(\"proxy.properties\");\n\n        ClientConfiguration awsClientConfig = ctx.getAwsClientConfig();\n\n        Assert.assertEquals(awsClientConfig.getProxyHost(), \"127.0.0.1\");\n        Assert.assertEquals(awsClientConfig.getProxyPort(), 80);\n        Assert.assertEquals(awsClientConfig.getProxyUsername(), \"fakeUser\");\n        Assert.assertEquals(awsClientConfig.getProxyPassword(), \"fakePassword\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/TestBasicMonkeyServer.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.MonkeyRunner;\nimport com.netflix.simianarmy.TestMonkey;\nimport com.netflix.simianarmy.basic.chaos.BasicChaosMonkey;\nimport com.netflix.simianarmy.chaos.TestChaosMonkeyContext;\n\n@SuppressWarnings(\"serial\")\npublic class TestBasicMonkeyServer extends BasicMonkeyServer {\n    private static final MonkeyRunner RUNNER = MonkeyRunner.getInstance();\n\n    private static boolean monkeyRan = false;\n\n    public static class SillyMonkey extends TestMonkey {\n        @Override\n        public void doMonkeyBusiness() {\n            monkeyRan = true;\n        }\n    }\n\n    @Override\n    public void addMonkeysToRun() {\n        MonkeyRunner.getInstance().replaceMonkey(BasicChaosMonkey.class, TestChaosMonkeyContext.class);\n        MonkeyRunner.getInstance().addMonkey(SillyMonkey.class);\n    }\n\n    @Test\n    public void testServer() {\n        BasicMonkeyServer server = new TestBasicMonkeyServer();\n\n        try {\n            server.init();\n        } catch (Exception e) {\n            Assert.fail(\"failed to init server\", e);\n        }\n\n        // there is a race condition since the monkeys will run\n        // in a different thread. On some systems we might\n        // need to add a sleep\n        Assert.assertTrue(monkeyRan, \"silly monkey ran\");\n\n        try {\n            server.destroy();\n        } catch (Exception e) {\n            Assert.fail(\"failed to destroy server\", e);\n        }\n        Assert.assertEquals(RUNNER.getMonkeys().size(), 1);\n        Assert.assertEquals(RUNNER.getMonkeys().get(0).getClass(), SillyMonkey.class);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/TestBasicRecorderEvent.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.MonkeyType;\n\npublic class TestBasicRecorderEvent {\n    public enum Types implements MonkeyType {\n        MONKEY\n    };\n\n    public enum EventTypes implements EventType {\n        EVENT\n    }\n\n    @Test\n    public void test() {\n        MonkeyType monkeyType = Types.MONKEY;\n        EventType eventType = EventTypes.EVENT;\n        BasicRecorderEvent evt = new BasicRecorderEvent(monkeyType, eventType, \"region\", \"test-id\");\n        testEvent(evt);\n\n        // CHECKSTYLE IGNORE MagicNumberCheck\n        long time = 1330538400000L;\n        evt = new BasicRecorderEvent(monkeyType, eventType, \"region\", \"test-id\", time);\n        testEvent(evt);\n        Assert.assertEquals(evt.eventTime().getTime(), time);\n\n    }\n\n    void testEvent(BasicRecorderEvent evt) {\n        Assert.assertEquals(evt.id(), \"test-id\");\n        Assert.assertEquals(evt.monkeyType(), Types.MONKEY);\n        Assert.assertEquals(evt.eventType(), EventTypes.EVENT);\n        Assert.assertEquals(evt.region(), \"region\");\n        Assert.assertEquals(evt.addField(\"a\", \"1\"), evt);\n        Map<String, String> map = new HashMap<String, String>();\n        map.put(\"b\", \"2\");\n        map.put(\"c\", \"3\");\n\n        Assert.assertEquals(evt.addFields(map), evt);\n        Assert.assertEquals(evt.field(\"a\"), \"1\");\n        Assert.assertEquals(evt.field(\"b\"), \"2\");\n        Assert.assertEquals(evt.field(\"c\"), \"3\");\n        Map<String, String> f = evt.fields();\n        Assert.assertEquals(f.get(\"a\"), \"1\");\n        Assert.assertEquals(f.get(\"b\"), \"2\");\n        Assert.assertEquals(f.get(\"c\"), \"3\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/TestBasicScheduler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic;\n\nimport static org.mockito.Mockito.when;\nimport static org.mockito.Mockito.mock;\n\nimport java.util.Calendar;\nimport java.util.concurrent.FutureTask;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.concurrent.atomic.AtomicLong;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.google.common.util.concurrent.Callables;\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyType;\nimport com.netflix.simianarmy.TestMonkeyContext;\n\n// CHECKSTYLE IGNORE MagicNumber\npublic class TestBasicScheduler {\n\n    @Test\n    public void testConstructors() {\n        BasicScheduler sched = new BasicScheduler();\n        Assert.assertNotNull(sched);\n        Assert.assertEquals(sched.frequency(), 1);\n        Assert.assertEquals(sched.frequencyUnit(), TimeUnit.HOURS);\n        BasicScheduler sched2 = new BasicScheduler(12, TimeUnit.MINUTES, 2);\n        Assert.assertEquals(sched2.frequency(), 12);\n        Assert.assertEquals(sched2.frequencyUnit(), TimeUnit.MINUTES);\n    }\n\n    private enum Enums implements MonkeyType {\n        MONKEY\n    };\n\n    private enum EventEnums implements EventType {\n        EVENT\n    }\n\n    @Test\n    public void testRunner() throws InterruptedException {\n        BasicScheduler sched = new BasicScheduler(200, TimeUnit.MILLISECONDS, 1);\n        Monkey mockMonkey = mock(Monkey.class);\n        when(mockMonkey.context()).thenReturn(new TestMonkeyContext(Enums.MONKEY));\n        when(mockMonkey.type()).thenReturn(Enums.MONKEY).thenReturn(Enums.MONKEY);\n\n        final AtomicLong counter = new AtomicLong(0L);\n        sched.start(mockMonkey, new Runnable() {\n            @Override\n            public void run() {\n                counter.incrementAndGet();\n            }\n        });\n        Thread.sleep(100);\n        Assert.assertEquals(counter.get(), 1);\n        Thread.sleep(200);\n        Assert.assertEquals(counter.get(), 2);\n        sched.stop(mockMonkey);\n        Thread.sleep(200);\n        Assert.assertEquals(counter.get(), 2);\n    }\n\n    @Test\n    public void testDelayedStart() throws Exception {\n        BasicScheduler sched = new BasicScheduler(1, TimeUnit.HOURS, 1);\n\n        TestMonkeyContext context = new TestMonkeyContext(Enums.MONKEY);\n        Monkey mockMonkey = mock(Monkey.class);\n        when(mockMonkey.context()).thenReturn(context).thenReturn(context);\n        when(mockMonkey.type()).thenReturn(Enums.MONKEY).thenReturn(Enums.MONKEY);\n\n        // first monkey has no previous events, so it runs practically immediately\n        FutureTask<Void> task = new FutureTask<Void>(Callables.<Void>returning(null));\n        sched.start(mockMonkey, task);\n        // make sure that the task gets completed within 100ms\n        task.get(100L, TimeUnit.MILLISECONDS);\n        sched.stop(mockMonkey);\n\n        // create an event 5 min ago\n        Calendar cal = Calendar.getInstance();\n        cal.add(Calendar.MINUTE, -5);\n        BasicRecorderEvent evt = new BasicRecorderEvent(\n            Enums.MONKEY, EventEnums.EVENT, \"region\", \"test-id\", cal.getTime().getTime());\n        context.recorder().recordEvent(evt);\n\n        // this time when it runs it will not run immediately since it should be scheduled for 55m from now.\n        task = new FutureTask<Void>(Callables.<Void>returning(null));\n        sched.start(mockMonkey, task);\n        try {\n            task.get(100, TimeUnit.MILLISECONDS);\n            Assert.fail(\"The task shouldn't have been completed in 100ms\");\n        } catch (TimeoutException e) { // NOPMD - This is an expected exception\n        }\n        sched.stop(mockMonkey);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/calendar/TestBavarianCalendar.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic.calendar;\n\nimport com.netflix.simianarmy.basic.BasicConfiguration;\nimport com.netflix.simianarmy.basic.calendars.BavarianCalendar;\nimport org.testng.Assert;\nimport org.testng.annotations.DataProvider;\nimport org.testng.annotations.Test;\n\nimport java.util.Calendar;\nimport java.util.Properties;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npublic class TestBavarianCalendar extends BavarianCalendar {\n    private static final Properties PROPS = new Properties();\n    private static final BasicConfiguration CFG = new BasicConfiguration(PROPS);\n\n    public TestBavarianCalendar() {\n        super(CFG);\n    }\n\n    private Calendar now = super.now();\n\n    @Override\n    public Calendar now() {\n        return (Calendar) now.clone();\n    }\n\n    private void setNow(Calendar now) {\n        this.now = now;\n    }\n\n    @DataProvider\n    public Object[][] easterDataProvider() {\n        return new Object[][] {\n                {1996, Calendar.APRIL, 7},\n                {1997, Calendar.MARCH, 30},\n                {1998, Calendar.APRIL, 12},\n                {1999, Calendar.APRIL, 4},\n                {2000, Calendar.APRIL, 23},\n                {2001, Calendar.APRIL, 15},\n                {2002, Calendar.MARCH, 31},\n                {2003, Calendar.APRIL, 20},\n                {2004, Calendar.APRIL, 11},\n                {2005, Calendar.MARCH, 27},\n                {2006, Calendar.APRIL, 16},\n                {2007, Calendar.APRIL, 8},\n                {2008, Calendar.MARCH, 23},\n                {2009, Calendar.APRIL, 12},\n                {2010, Calendar.APRIL, 4},\n                {2011, Calendar.APRIL, 24},\n                {2012, Calendar.APRIL, 8},\n                {2013, Calendar.MARCH, 31},\n                {2014, Calendar.APRIL, 20},\n                {2015, Calendar.APRIL, 5},\n                {2016, Calendar.MARCH, 27},\n                {2017, Calendar.APRIL, 16},\n                {2018, Calendar.APRIL, 1},\n                {2019, Calendar.APRIL, 21},\n                {2020, Calendar.APRIL, 12},\n                {2021, Calendar.APRIL, 4},\n                {2022, Calendar.APRIL, 17},\n                {2023, Calendar.APRIL, 9},\n                {2024, Calendar.MARCH, 31},\n                {2025, Calendar.APRIL, 20},\n                {2026, Calendar.APRIL, 5},\n                {2027, Calendar.MARCH, 28},\n                {2028, Calendar.APRIL, 16},\n                {2029, Calendar.APRIL, 1},\n                {2030, Calendar.APRIL, 21},\n                {2031, Calendar.APRIL, 13},\n                {2032, Calendar.MARCH, 28},\n                {2033, Calendar.APRIL, 17},\n                {2034, Calendar.APRIL, 9},\n                {2035, Calendar.MARCH, 25},\n                {2036, Calendar.APRIL, 13},\n        };\n    }\n\n    @Test(dataProvider = \"easterDataProvider\")\n    public void testEaster(int year, int month, int dayOfMonth) {\n        Assert.assertEquals(dayOfYear(year, month, dayOfMonth), westernEasterDayOfYear(year));\n    }\n\n    @DataProvider\n    public Object[][] holidayDataProvider() {\n        return new Object[][] {\n                {2016, Calendar.JANUARY, 1},   // new year\n                {2016, Calendar.JANUARY, 6},   // epiphanie\n                {2016, Calendar.MARCH, 25},    // good friday\n                {2016, Calendar.MARCH, 28},    // easter monday\n                {2016, Calendar.MAY, 1},       // labor day\n                {2016, Calendar.MAY, 5},       // ascension day\n                {2016, Calendar.MAY, 6},       // friday after ascension day\n                {2016, Calendar.MAY, 16},      // whit monday\n                {2016, Calendar.MAY, 26},      // corpus christi\n                {2016, Calendar.AUGUST, 15},   // assumption day\n                {2016, Calendar.OCTOBER, 3},   // german unity day\n                {2016, Calendar.DECEMBER, 24}, // christmas holidays\n                {2016, Calendar.DECEMBER, 25},\n                {2016, Calendar.DECEMBER, 26},\n                {2016, Calendar.DECEMBER, 27},\n                {2016, Calendar.DECEMBER, 28},\n                {2016, Calendar.DECEMBER, 29},\n                {2016, Calendar.DECEMBER, 30},\n                {2016, Calendar.DECEMBER, 31},\n                // now, \"bridge days\"\n                {2015, Calendar.JANUARY, 2},   // friday after new year\n                {2015, Calendar.JANUARY, 5},   // monday before epiphanie\n                {2011, Calendar.JANUARY, 7},   // friday after epiphanie\n                {2012, Calendar.APRIL, 30},    // monday before labor day\n                {2014, Calendar.MAY, 2},       // friday after labor day\n                {2006, Calendar.AUGUST, 14},   // monday before assumption day\n                {2013, Calendar.AUGUST, 16},   // friday after assumption day\n                {2006, Calendar.OCTOBER, 2},   // monday before german unity day\n                {2013, Calendar.OCTOBER, 4},   // friday after german unity day\n                {2011, Calendar.OCTOBER, 31},  // monday before all saints\n                {2012, Calendar.NOVEMBER, 2},  // friday after all saints\n                {2013, Calendar.DECEMBER, 23}  // monday before christas eve\n        };\n    }\n\n    @Test(dataProvider = \"holidayDataProvider\")\n    public void testHolidays(int year, int month, int dayOfMonth) {\n        Calendar test = Calendar.getInstance();\n        test.set(Calendar.YEAR, year);\n        test.set(Calendar.MONTH, month);\n        test.set(Calendar.DAY_OF_MONTH, dayOfMonth);\n        test.set(Calendar.HOUR_OF_DAY, 10);\n        setNow(test);\n\n        Assert.assertTrue(isHoliday(test), test.getTime().toString() + \" is a holiday?\");\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/chaos/TestBasicChaosEmailNotifier.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic.chaos;\n\nimport static org.mockito.Mockito.doNothing;\nimport static org.mockito.Mockito.spy;\nimport static org.mockito.Mockito.verify;\n\nimport java.util.Properties;\n\nimport org.testng.Assert;\nimport org.testng.annotations.BeforeMethod;\nimport org.testng.annotations.Test;\n\nimport com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;\nimport com.netflix.simianarmy.GroupType;\nimport com.netflix.simianarmy.basic.BasicConfiguration;\nimport com.netflix.simianarmy.chaos.TestChaosMonkeyContext.TestInstanceGroup;\n\npublic class TestBasicChaosEmailNotifier {\n\n    private final AmazonSimpleEmailServiceClient sesClient = new AmazonSimpleEmailServiceClient();\n\n    private BasicChaosEmailNotifier basicChaosEmailNotifier;\n\n    private Properties properties;\n\n    private enum GroupTypes implements GroupType {\n        TYPE_A\n    };\n\n    private String name = \"name0\";\n    private String region = \"reg1\";\n    private String to = \"foo@bar.com\";\n    private String instanceId = \"i-12345678901234567\";\n    private String subjectPrefix = \"Subject Prefix - \";\n    private String subjectSuffix = \" - Subject Suffix \";\n    private String bodyPrefix = \"Body Prefix - \";\n    private String bodySuffix = \" - Body Suffix\";\n\n    private final TestInstanceGroup testInstanceGroup = new TestInstanceGroup(GroupTypes.TYPE_A, name, region, \"0:\"\n            + instanceId);\n\n    private String defaultBody = \"Instance \" + instanceId + \" of \" + GroupTypes.TYPE_A + \" \" + name\n            + \" is being terminated by Chaos monkey.\";\n\n    private String defaultSubject = \"Chaos Monkey Termination Notification for \" + to;\n\n    @BeforeMethod\n    public void beforeMethod() {\n        properties = new Properties();\n    }\n\n    @Test\n    public void testInvalidEmailAddresses() {\n        String[] invalidEmails = new String[] { \"username\",\n                                                \"username@.com.my\",\n                                                \"username123@example.a\",\n                                                \"username123@.com\",\n                                                \"username123@.com.com\",\n                                                \"username()*@example.com\",\n                                                \"username@%*.com\"};\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n\n        for (String emailAddress : invalidEmails) {\n            Assert.assertFalse(basicChaosEmailNotifier.isValidEmail(emailAddress));\n        }\n    }\n\n    @Test\n    public void testValidEmailAddresses() {\n        String[] validEmails = new String[] { \"username-100@example.com\",\n                                              \"name.surname+ml-info@example.com\",\n                                              \"username.100@example.com\",\n                                              \"username111@example.com\",\n                                              \"username-100@username.net\",\n                                              \"username.100@example.com.au\",\n                                              \"username@1.com\",\n                                              \"username@example.com\",\n                                              \"username+100@example.com\",\n                                              \"username-100@example-test.com\" };\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n\n        for (String emailAddress : validEmails) {\n            Assert.assertTrue(basicChaosEmailNotifier.isValidEmail(emailAddress));\n        }\n    }\n\n    @Test\n    public void testbuildEmailSubject() {\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n        String subject = basicChaosEmailNotifier.buildEmailSubject(to);\n        Assert.assertEquals(subject, defaultSubject);\n    }\n\n    @Test\n    public void testbuildEmailSubjectWithSubjectPrefix() {\n        properties.setProperty(\"simianarmy.chaos.notification.subject.prefix\", subjectPrefix);\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n        String subject = basicChaosEmailNotifier.buildEmailSubject(to);\n        Assert.assertEquals(subject, subjectPrefix + defaultSubject);\n    }\n\n    @Test\n    public void testbuildEmailSubjectWithSubjectSuffix() {\n        properties.setProperty(\"simianarmy.chaos.notification.subject.suffix\", subjectSuffix);\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n        String subject = basicChaosEmailNotifier.buildEmailSubject(to);\n        Assert.assertEquals(subject, defaultSubject + subjectSuffix);\n    }\n\n    @Test\n    public void testbuildEmailSubjectWithSubjectPrefixSuffix() {\n        properties.setProperty(\"simianarmy.chaos.notification.subject.prefix\", subjectPrefix);\n        properties.setProperty(\"simianarmy.chaos.notification.subject.suffix\", subjectSuffix);\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n        String subject = basicChaosEmailNotifier.buildEmailSubject(to);\n        Assert.assertEquals(subject, subjectPrefix + defaultSubject + subjectSuffix);\n    }\n\n    @Test\n    public void testbuildEmailBody() {\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n        String subject = basicChaosEmailNotifier.buildEmailBody(testInstanceGroup, instanceId, null);\n        Assert.assertEquals(subject, defaultBody);\n    }\n\n    @Test\n    public void testbuildEmailBodyPrefix() {\n        properties.setProperty(\"simianarmy.chaos.notification.body.prefix\", bodyPrefix);\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n        String subject = basicChaosEmailNotifier.buildEmailBody(testInstanceGroup, instanceId, null);\n        Assert.assertEquals(subject, bodyPrefix + defaultBody);\n    }\n\n    @Test\n    public void testbuildEmailBodySuffix() {\n        properties.setProperty(\"simianarmy.chaos.notification.body.suffix\", bodySuffix);\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n        String subject = basicChaosEmailNotifier.buildEmailBody(testInstanceGroup, instanceId, null);\n        Assert.assertEquals(subject, defaultBody + bodySuffix);\n    }\n\n    @Test\n    public void testbuildEmailBodyPrefixSuffix() {\n        properties.setProperty(\"simianarmy.chaos.notification.body.prefix\", bodyPrefix);\n        properties.setProperty(\"simianarmy.chaos.notification.body.suffix\", bodySuffix);\n        basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null);\n        String subject = basicChaosEmailNotifier.buildEmailBody(testInstanceGroup, instanceId, null);\n        Assert.assertEquals(subject, bodyPrefix + defaultBody + bodySuffix);\n    }\n\n    @Test\n    public void testBuildAndSendEmail() {\n        properties.setProperty(\"simianarmy.chaos.notification.sourceEmail\", to);\n        BasicChaosEmailNotifier spyBasicChaosEmailNotifier = spy(new BasicChaosEmailNotifier(new BasicConfiguration(\n                properties), sesClient, null));\n        doNothing().when(spyBasicChaosEmailNotifier).sendEmail(to, defaultSubject, defaultBody);\n        spyBasicChaosEmailNotifier.buildAndSendEmail(to, testInstanceGroup, instanceId, null);\n        verify(spyBasicChaosEmailNotifier).sendEmail(to, defaultSubject, defaultBody);\n    }\n\n    @Test\n    public void testBuildAndSendEmailSubjectIsBody() {\n        properties.setProperty(\"simianarmy.chaos.notification.subject.isBody\", \"true\");\n        properties.setProperty(\"simianarmy.chaos.notification.sourceEmail\", to);\n        BasicChaosEmailNotifier spyBasicChaosEmailNotifier = spy(new BasicChaosEmailNotifier(new BasicConfiguration(\n                properties), sesClient, null));\n        doNothing().when(spyBasicChaosEmailNotifier).sendEmail(to, defaultBody, defaultBody);\n        spyBasicChaosEmailNotifier.buildAndSendEmail(to, testInstanceGroup, instanceId, null);\n        verify(spyBasicChaosEmailNotifier).sendEmail(to, defaultBody, defaultBody);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/chaos/TestBasicChaosInstanceSelector.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic.chaos;\n\nimport java.util.*;\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.netflix.simianarmy.GroupType;\nimport com.netflix.simianarmy.chaos.ChaosInstanceSelector;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\n\nimport org.testng.annotations.Test;\nimport org.testng.annotations.DataProvider;\nimport org.testng.Assert;\nimport org.slf4j.Logger;\n\nimport static org.slf4j.helpers.NOPLogger.NOP_LOGGER;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\npublic class TestBasicChaosInstanceSelector {\n    private ChaosInstanceSelector selector = new BasicChaosInstanceSelector() {\n        // turn off selector logger for this test since we call it ~1M times\n        protected Logger logger() {\n            return NOP_LOGGER;\n        }\n    };\n\n    public enum Types implements GroupType {\n        TEST\n    }\n\n    private InstanceGroup group = new InstanceGroup() {\n        public GroupType type() {\n            return Types.TEST;\n        }\n\n        public String name() {\n            return \"TestGroup\";\n        }\n\n        public String region() {\n            return \"region\";\n        }\n\n        public List<TagDescription> tags() {\n            return Collections.<TagDescription>emptyList();\n        }\n\n        public List<String> instances() {\n            return Arrays.asList(\"i-123456789012345670\", \"i-123456789012345671\", \"i-123456789012345672\", \"i-123456789012345673\", \"i-123456789012345674\",\n                    \"i-123456789012345675\", \"i-123456789012345676\", \"i-123456789012345677\", \"i-123456789012345678\", \"i-123456789012345679\");\n        }\n\n        public void addInstance(String ignored) {\n        }\n\n        @Override\n        public InstanceGroup copyAs(String name) {\n            return this;\n        }\n    };\n\n    @Test\n    public void testSelect() {\n        Assert.assertTrue(selector.select(group, 0).isEmpty(), \"select disabled group is always null\");\n        Assert.assertTrue(selector.select(group, 0.0).isEmpty(), \"select disabled group is always null\");\n\n        int selected = 0;\n        for (int i = 0; i < 100; i++) {\n            selected += selector.select(group, 1.0).size();\n        }\n\n        Assert.assertEquals(selected, 100, \"1.0 probability always selects an instance\");\n\n    }\n\n    @DataProvider\n    public Object[][] evenSelectionDataProvider() {\n        return new Object[][] {{1.0}, {0.9}, {0.8}, {0.7}, {0.6}, {0.5}, {0.4}, {0.3}, {0.2},\n                {0.1} };\n    }\n\n    static final int RUNS = 1000000;\n\n    @Test(dataProvider = \"evenSelectionDataProvider\")\n    public void testEvenSelections(double probability) {\n\n        Map<String, Integer> selectMap = new HashMap<String, Integer>();\n        for (int i = 0; i < RUNS; i++) {\n            Collection<String> instances = selector.select(group, probability);\n            for (String inst : instances) {\n                if (selectMap.containsKey(inst)) {\n                    selectMap.put(inst, selectMap.get(inst) + 1);\n                } else {\n                    selectMap.put(inst, 1);\n                }\n            }\n        }\n\n        Assert.assertEquals(selectMap.size(), group.instances().size(), \"verify we selected all instances\");\n\n        // allow for 4% variation over all the selection runs\n        int avg = Double.valueOf((RUNS / (double) group.instances().size()) * probability).intValue();\n        int max = Double.valueOf(avg + (avg * 0.04)).intValue();\n        int min = Double.valueOf(avg - (avg * 0.04)).intValue();\n\n        for (Map.Entry<String, Integer> pair : selectMap.entrySet()) {\n            Assert.assertTrue(pair.getValue() > min && pair.getValue() < max, pair.getKey() + \" selected \" + avg\n                    + \" +- 4% times for prob: \" + probability + \" [got: \" + pair.getValue() + \"]\");\n        }\n    }\n\n    @Test\n    public void testSelectWithProbMoreThanOne() {\n        // The number of selected instances should always be p when the prob is an integer.\n        for (int p = 0; p <= group.instances().size(); p++) {\n            Assert.assertEquals(selector.select(group, p).size(), p);\n        }\n\n        // When the prob is bigger than the size of the group, we get the whole group.\n        for (int p = group.instances().size(); p <= group.instances().size() * 2; p++) {\n            Assert.assertEquals(selector.select(group, p).size(), group.instances().size());\n        }\n    }\n\n    @Test\n    public void testSelectWithProbMoreThanOneWithFraction() {\n        // The number of selected instances can be p or p+1, depending on whether the fraction part\n        // can get a instance selected.\n        for (int p = 0; p <= group.instances().size(); p++) {\n            Collection<String> selected = selector.select(group, p + 0.5);\n            Assert.assertTrue(selected.size() >= p && selected.size() <= p + 1);\n        }\n\n        // When the prob is bigger than the size of the group, we get the whole group.\n        for (int p = group.instances().size(); p <= group.instances().size() * 2; p++) {\n            Collection<String> selected = selector.select(group, p + 0.5);\n            Assert.assertEquals(selected.size(), group.instances().size());\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/chaos/TestBasicChaosMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic.chaos;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport javax.ws.rs.core.Response;\n\nimport com.netflix.simianarmy.GroupType;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.Monkey;\nimport com.netflix.simianarmy.MonkeyScheduler;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport com.netflix.simianarmy.chaos.ChaosMonkey;\nimport com.netflix.simianarmy.chaos.TestChaosMonkeyContext;\nimport com.netflix.simianarmy.resources.chaos.ChaosMonkeyResource;\nimport com.amazonaws.services.autoscaling.model.TagDescription;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\npublic class TestBasicChaosMonkey {\n    private enum GroupTypes implements GroupType {\n        TYPE_A, TYPE_B\n    };\n\n    @Test\n    public void testDisabled() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"disabled.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 0, \"no groups selected on\");\n        Assert.assertEquals(terminated.size(), 0, \"nothing terminated\");\n    }\n\n    @Test\n    public void testEnabledA() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"enabledA.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 2);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name1\");\n        Assert.assertEquals(terminated.size(), 0, \"nothing terminated\");\n    }\n\n    @Test\n    public void testUnleashedEnabledA() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"unleashedEnabledA.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 2);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name1\");\n        Assert.assertEquals(terminated.size(), 2);\n        Assert.assertEquals(terminated.get(0), \"0:i-123456789012345670\");\n        Assert.assertEquals(terminated.get(1), \"1:i-123456789012345671\");\n    }\n\n    @Test\n    public void testEnabledB() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"enabledB.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 2);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name2\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name3\");\n        Assert.assertEquals(terminated.size(), 0, \"nothing terminated\");\n    }\n\n    @Test\n    public void testUnleashedEnabledB() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"unleashedEnabledB.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 2);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name2\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name3\");\n        Assert.assertEquals(terminated.size(), 2);\n        Assert.assertEquals(terminated.get(0), \"2:i-123456789012345672\");\n        Assert.assertEquals(terminated.get(1), \"3:i-123456789012345673\");\n    }\n\n    @Test\n    public void testEnabledAwithout1() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"enabledAwithout1.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 1);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(terminated.size(), 1);\n        Assert.assertEquals(terminated.get(0), \"0:i-123456789012345670\");\n    }\n\n    @Test\n    public void testEnabledAwith0() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"enabledAwith0.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 1);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(terminated.size(), 1);\n        Assert.assertEquals(terminated.get(0), \"0:i-123456789012345670\");\n    }\n\n    @Test\n    public void testAll() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"all.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 4);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name1\");\n        Assert.assertEquals(selectedOn.get(2).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(2).name(), \"name2\");\n        Assert.assertEquals(selectedOn.get(3).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(3).name(), \"name3\");\n        Assert.assertEquals(terminated.size(), 4);\n        Assert.assertEquals(terminated.get(0), \"0:i-123456789012345670\");\n        Assert.assertEquals(terminated.get(1), \"1:i-123456789012345671\");\n        Assert.assertEquals(terminated.get(2), \"2:i-123456789012345672\");\n        Assert.assertEquals(terminated.get(3), \"3:i-123456789012345673\");\n    }\n\n    @Test\n    public void testNoProbability() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"noProbability.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 4);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name1\");\n        Assert.assertEquals(selectedOn.get(2).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(2).name(), \"name2\");\n        Assert.assertEquals(selectedOn.get(3).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(3).name(), \"name3\");\n        Assert.assertEquals(terminated.size(), 0);\n    }\n\n    @Test\n    public void testFullProbability() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"fullProbability.properties\") {\n            @Override\n            public MonkeyScheduler scheduler() {\n                return new MonkeyScheduler() {\n                    @Override\n                    public int frequency() {\n                        return 1;\n                    }\n\n                    @Override\n                    public TimeUnit frequencyUnit() {\n                        return TimeUnit.DAYS;\n                    }\n\n                    @Override\n                    public void start(Monkey monkey, Runnable run) {\n                        Assert.assertEquals(monkey.type().name(), monkey.type().name(), \"starting monkey\");\n                        run.run();\n                    }\n\n                    @Override\n                    public void stop(Monkey monkey) {\n                        Assert.assertEquals(monkey.type().name(), monkey.type().name(), \"stopping monkey\");\n                    }\n                };\n            }\n        };\n\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 4);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name1\");\n        Assert.assertEquals(selectedOn.get(2).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(2).name(), \"name2\");\n        Assert.assertEquals(selectedOn.get(3).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(3).name(), \"name3\");\n        Assert.assertEquals(terminated.size(), 4);\n    }\n\n\n    @Test\n    public void testNoProbabilityByName() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"noProbabilityByName.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(selectedOn.size(), 4);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name1\");\n        Assert.assertEquals(selectedOn.get(2).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(2).name(), \"name2\");\n        Assert.assertEquals(selectedOn.get(3).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B);\n        Assert.assertEquals(selectedOn.get(3).name(), \"name3\");\n        Assert.assertEquals(terminated.size(), 0);\n    }\n\n    @Test\n    public void testMaxTerminationCountPerDayAsZero() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"terminationPerDayAsZero.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 0);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n    }\n\n    @Test\n    public void testMaxTerminationCountPerDayAsOne() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"terminationPerDayAsOne.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n\n        // Run the chaos the second time will NOT trigger another termination\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n    }\n\n    @Test\n    public void testMaxTerminationCountPerDayAsBiggerThanOne() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"terminationPerDayAsBiggerThanOne.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n\n        // Run the chaos the second time will trigger another termination\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 2);\n        Assert.assertEquals(ctx.terminated().size(), 2);\n    }\n\n    @Test\n    public void testMaxTerminationCountPerDayAsSmallerThanOne() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"terminationPerDayAsSmallerThanOne.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n\n        // Run the chaos the second time will NOT trigger another termination\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n    }\n\n    @Test\n    public void testMaxTerminationCountPerDayAsNegative() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"terminationPerDayAsNegative.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 0);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n    }\n\n    @Test\n    public void testMaxTerminationCountPerDayAsVerySmall() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"terminationPerDayAsVerySmall.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 0);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n    }\n\n    @Test\n    public void testMaxTerminationCountPerDayGroupLevel() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"terminationPerDayGroupLevel.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n\n        for (int i = 1; i <= 3; i++) {\n            chaos.start();\n            chaos.stop();\n            Assert.assertEquals(ctx.selectedOn().size(), i);\n            Assert.assertEquals(ctx.terminated().size(), i);\n        }\n        // Run the chaos the second time will NOT trigger another termination\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 3);\n        Assert.assertEquals(ctx.terminated().size(), 3);\n    }\n\n    @Test\n    public void testGetValueFromCfgWithDefault() {\n\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"propertiesWithDefaults.properties\");\n        BasicChaosMonkey chaos = new BasicChaosMonkey(ctx);\n\n        // named 1 has actual values in config\n        InstanceGroup named1 = new BasicInstanceGroup(\"named1\", GroupTypes.TYPE_A, \"test-dev-1\", Collections.<TagDescription>emptyList());\n\n        // named 2 doesn't have values but it's group has values\n        InstanceGroup named2 = new BasicInstanceGroup(\"named2\", GroupTypes.TYPE_A, \"test-dev-1\", Collections.<TagDescription>emptyList());\n\n        // named 3 doesn't have values and it's group doesn't have values\n        InstanceGroup named3 = new BasicInstanceGroup(\"named3\", GroupTypes.TYPE_B, \"test-dev-1\", Collections.<TagDescription>emptyList());\n\n        Assert.assertEquals(chaos.getBoolFromCfgOrDefault(named1, \"enabled\", true), false);\n        Assert.assertEquals(chaos.getNumFromCfgOrDefault(named1, \"probability\", 3.0), 1.1);\n        Assert.assertEquals(chaos.getNumFromCfgOrDefault(named1, \"maxTerminationsPerDay\", 4.0), 2.1);\n\n        Assert.assertEquals(chaos.getBoolFromCfgOrDefault(named2, \"enabled\", true), true);\n        Assert.assertEquals(chaos.getNumFromCfgOrDefault(named2, \"probability\", 3.0), 1.0);\n        Assert.assertEquals(chaos.getNumFromCfgOrDefault(named2, \"maxTerminationsPerDay\", 4.0), 2.0);\n\n        Assert.assertEquals(chaos.getBoolFromCfgOrDefault(named3, \"enabled\", true), true);\n        Assert.assertEquals(chaos.getNumFromCfgOrDefault(named3, \"probability\", 3.0), 3.0);\n        Assert.assertEquals(chaos.getNumFromCfgOrDefault(named3, \"maxTerminationsPerDay\", 4.0), 4.0);\n    }\n\n    @Test\n    public void testMandatoryTerminationDisabled() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"mandatoryTerminationDisabled.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n    }\n\n    @Test\n    public void testMandatoryTerminationNotDefined() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"mandatoryTerminationNotDefined.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n    }\n\n    @Test\n    public void testMandatoryTerminationNoOptInTime() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"mandatoryTerminationNoOptInTime.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n    }\n\n    @Test\n    public void testMandatoryTerminationInsideWindow() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"mandatoryTerminationInsideWindow.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        // The last opt-in time is within the window, so no mandatory termination is triggered\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n    }\n\n    @Test\n    public void testMandatoryTerminationOutsideWindow() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"mandatoryTerminationOutsideWindow.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        // There was no termination in the last window, so one mandatory termination is triggered\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n    }\n\n    @Test\n    public void testMandatoryTerminationOutsideWindowWithPreviousTermination() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"mandatoryTerminationOutsideWindow.properties\");\n        terminateOnDemand(ctx, \"TYPE_C\", \"name4\");\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        // There was termination in the last window, so no mandatory termination is triggered\n        Assert.assertEquals(ctx.selectedOn().size(), 2);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n    }\n\n    @Test\n    public void testMandatoryTerminationInsideWindowWithPreviousTermination() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"mandatoryTerminationInsideWindow.properties\");\n        terminateOnDemand(ctx, \"TYPE_C\", \"name4\");\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        // There was termination in the last window, so no mandatory termination is triggered\n        Assert.assertEquals(ctx.selectedOn().size(), 2);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n    }\n\n    @Test\n    public void testNotificationEnabled() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"notificationEnabled.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 4);\n        Assert.assertEquals(ctx.terminated().size(), 4);\n        // Notification is enabled only for 2 terminations.\n        Assert.assertEquals(ctx.getNotified(), 2);\n    }\n\n    @Test\n    public void testGlobalNotificationEnabled() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"globalNotificationEnabled.properties\");\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 4);\n        Assert.assertEquals(ctx.terminated().size(), 4);\n        Assert.assertEquals(ctx.getNotified(), 1);\n        Assert.assertEquals(ctx.getGloballyNotified(), 4);\n    }\n\n    private void terminateOnDemand(TestChaosMonkeyContext ctx, String groupType, String groupName) {\n        String input = String.format(\"{\\\"eventType\\\":\\\"CHAOS_TERMINATION\\\",\\\"groupType\\\":\\\"%s\\\",\\\"groupName\\\":\\\"%s\\\"}\",\n                groupType, groupName);\n\n        int currentSelectedOn = ctx.selectedOn().size();\n        int currentTerminated = ctx.terminated().size();\n\n        ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.OK);\n        Assert.assertEquals(ctx.selectedOn().size(), currentSelectedOn + 1);\n        Assert.assertEquals(ctx.terminated().size(), currentTerminated + 1);\n    }\n\n    private void validateAddEventResult(ChaosMonkeyResource resource, String input, Response.Status responseStatus) {\n        try {\n            Response resp = resource.addEvent(input);\n            Assert.assertEquals(resp.getStatus(), responseStatus.getStatusCode());\n        } catch (Exception e) {\n            Assert.fail(\"addEvent throws exception\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/chaos/TestCloudFormationChaosMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.basic.chaos;\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\nimport static org.testng.Assert.assertEquals;\nimport static org.testng.Assert.assertTrue;\nimport static org.testng.Assert.assertFalse;\n\nimport com.netflix.simianarmy.chaos.TestChaosMonkeyContext;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\n\nimport java.util.Collections;\n\npublic class TestCloudFormationChaosMonkey {\n\n    public static final long EXPECTED_MILLISECONDS = 2000;\n\n    @Test\n    public void testIsGroupEnabled() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"cloudformation.properties\");\n        CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx);\n        InstanceGroup group1 = new BasicInstanceGroup(\"new-group-TestGroup1-XCFNFNFNF\",\n                TestChaosMonkeyContext.CrawlerTypes.TYPE_D, \"region\", Collections.<TagDescription>emptyList());\n        InstanceGroup group2 = new BasicInstanceGroup(\"new-group-TestGroup2-XCFNGHFNF\",\n                TestChaosMonkeyContext.CrawlerTypes.TYPE_D, \"region\", Collections.<TagDescription>emptyList());\n        assertTrue(chaos.isGroupEnabled(group1));\n        assertFalse(chaos.isGroupEnabled(group2));\n    }\n\n    @Test\n    public void testIsMaxTerminationCountExceeded() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"cloudformation.properties\");\n        CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx);\n        InstanceGroup group1 = new BasicInstanceGroup(\"new-group-TestGroup1-XCFNFNFNF\",\n                TestChaosMonkeyContext.CrawlerTypes.TYPE_D, \"region\", Collections.<TagDescription>emptyList());\n        assertFalse(chaos.isMaxTerminationCountExceeded(group1));\n    }\n\n    @Test\n    public void testGetEffectiveProbability() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"cloudformation.properties\");\n        CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx);\n        InstanceGroup group1 = new BasicInstanceGroup(\"new-group-TestGroup1-XCFNFNFNF\",\n                TestChaosMonkeyContext.CrawlerTypes.TYPE_D, \"region\", Collections.<TagDescription>emptyList());\n        assertEquals(1.0, chaos.getEffectiveProbability(group1));\n    }\n\n    @Test\n    public void testNoSuffixInstanceGroup() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"disabled.properties\");\n        CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx);\n        InstanceGroup group = new BasicInstanceGroup(\"new-group-TestGroup-XCFNFNFNF\",\n                TestChaosMonkeyContext.CrawlerTypes.TYPE_D, \"region\", Collections.<TagDescription>emptyList());\n        InstanceGroup newGroup = chaos.noSuffixInstanceGroup(group);\n        assertEquals(newGroup.name(), \"new-group-TestGroup\");\n    }\n\n    @Test\n    public void testGetLastOptInMilliseconds() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"cloudformation.properties\");\n        CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx);\n        InstanceGroup group = new BasicInstanceGroup(\"new-group-TestGroup1-XCFNFNFNF\",\n                TestChaosMonkeyContext.CrawlerTypes.TYPE_D, \"region\", Collections.<TagDescription>emptyList());\n        assertEquals(chaos.getLastOptInMilliseconds(group), EXPECTED_MILLISECONDS);\n    }\n\n    @Test\n    public void testCloudFormationChaosMonkeyIntegration() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"cloudformation.properties\");\n        CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n        Assert.assertEquals(ctx.getNotified(), 1);\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/basic/janitor/TestBasicJanitorRuleEngine.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\npackage com.netflix.simianarmy.basic.janitor;\n\nimport com.netflix.simianarmy.Resource;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.janitor.Rule;\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport java.util.Date;\n\npublic class TestBasicJanitorRuleEngine {\n\n    @Test\n    public void testEmptyRuleSet() {\n        Resource resource = new AWSResource().withId(\"id\");\n        BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine();\n        Assert.assertTrue(engine.isValid(resource));\n    }\n\n    @Test\n    public void testAllValid() {\n        Resource resource = new AWSResource().withId(\"id\");\n        BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine()\n        .addRule(new AlwaysValidRule())\n        .addRule(new AlwaysValidRule())\n        .addRule(new AlwaysValidRule());\n        Assert.assertTrue(engine.isValid(resource));\n    }\n\n    @Test\n    public void testMixed() {\n        Resource resource = new AWSResource().withId(\"id\");\n        DateTime now = DateTime.now();\n        BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine()\n        .addRule(new AlwaysValidRule())\n        .addRule(new AlwaysInvalidRule(now, 1))\n        .addRule(new AlwaysValidRule());\n        Assert.assertFalse(engine.isValid(resource));\n    }\n\n    @Test\n    public void testIsValidWithNearestTerminationTime() {\n        int[][] permutaions = {{1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 1}, {3, 1, 2}, {3, 2, 1}};\n\n        for (int[] perm : permutaions) {\n            Resource resource = new AWSResource().withId(\"id\");\n            DateTime now = DateTime.now();\n            BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine()\n            .addRule(new AlwaysInvalidRule(now, perm[0]))\n            .addRule(new AlwaysInvalidRule(now, perm[1]))\n            .addRule(new AlwaysInvalidRule(now, perm[2]));\n            Assert.assertFalse(engine.isValid(resource));\n            Assert.assertEquals(\n                    resource.getExpectedTerminationTime().getTime(),\n                    now.plusDays(1).getMillis());\n            Assert.assertEquals(resource.getTerminationReason(), \"1\");\n        }\n    }\n\n    @Test void testWithExclusionRuleMatch1() {\n        Resource resource = new AWSResource().withId(\"id\");\n        DateTime now = DateTime.now();\n        BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine()\n                .addExclusionRule(new AlwaysValidRule())\n                .addRule(new AlwaysInvalidRule(now, 1));\n        Assert.assertTrue(engine.isValid(resource));\n    }\n\n    @Test void testWithExclusionRuleMatch2() {\n        Resource resource = new AWSResource().withId(\"id\");\n        DateTime now = DateTime.now();\n        BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine()\n                .addExclusionRule(new AlwaysValidRule())\n                .addRule(new AlwaysValidRule());\n        Assert.assertTrue(engine.isValid(resource));\n    }\n\n    @Test void testWithExclusionRuleNotMatch1() {\n        Resource resource = new AWSResource().withId(\"id\");\n        DateTime now = DateTime.now();\n        BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine()\n                .addExclusionRule(new AlwaysInvalidRule(now, 1))\n                .addRule(new AlwaysInvalidRule(now, 1));\n        Assert.assertFalse(engine.isValid(resource));\n    }\n\n    @Test void testWithExclusionRuleNotMatch2() {\n        Resource resource = new AWSResource().withId(\"id\");\n        DateTime now = DateTime.now();\n        BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine()\n                .addExclusionRule(new AlwaysInvalidRule(now, 1))\n                .addRule(new AlwaysValidRule());\n        Assert.assertTrue(engine.isValid(resource));\n    }\n}\n\nclass AlwaysValidRule implements Rule {\n    @Override\n    public boolean isValid(Resource resource) {\n        return true;\n    }\n}\n\nclass AlwaysInvalidRule implements Rule {\n    private final int retentionDays;\n    private final DateTime now;\n\n    public AlwaysInvalidRule(DateTime now, int retentionDays) {\n        this.retentionDays = retentionDays;\n        this.now = now;\n    }\n\n    @Override\n    public boolean isValid(Resource resource) {\n        resource.setExpectedTerminationTime(\n                new Date(now.plusDays(retentionDays).getMillis()));\n        resource.setTerminationReason(String.valueOf(retentionDays));\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/chaos/TestChaosMonkeyArmy.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.chaos;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Properties;\n\nimport org.testng.Assert;\nimport org.testng.annotations.BeforeTest;\nimport org.testng.annotations.Test;\n\nimport com.google.common.base.Charsets;\nimport com.google.common.io.Files;\nimport com.netflix.simianarmy.basic.chaos.BasicChaosMonkey;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport com.netflix.simianarmy.chaos.TestChaosMonkeyContext.Notification;\nimport com.netflix.simianarmy.chaos.TestChaosMonkeyContext.SshAction;\n\n// CHECKSTYLE IGNORE MagicNumberCheck\npublic class TestChaosMonkeyArmy {\n    private File sshKey;\n\n    @BeforeTest\n    public void createSshKey() throws IOException {\n        sshKey = File.createTempFile(\"tmp\", \"key\");\n        Files.write(\"fakekey\", sshKey, Charsets.UTF_8);\n        sshKey.deleteOnExit();\n    }\n\n    private TestChaosMonkeyContext runChaosMonkey(String key) {\n        return runChaosMonkey(key, true);\n    }\n\n    private TestChaosMonkeyContext runChaosMonkey(String key, boolean burnMoney) {\n        Properties properties = new Properties();\n        properties.setProperty(\"simianarmy.chaos.enabled\", \"true\");\n        properties.setProperty(\"simianarmy.chaos.leashed\", \"false\");\n        properties.setProperty(\"simianarmy.chaos.TYPE_A.enabled\", \"true\");\n        properties.setProperty(\"simianarmy.chaos.notification.global.enabled\", \"true\");\n\n        properties.setProperty(\"simianarmy.chaos.burnmoney\", Boolean.toString(burnMoney));\n\n        properties.setProperty(\"simianarmy.chaos.shutdowninstance.enabled\", \"false\");\n        properties.setProperty(\"simianarmy.chaos.\" + key.toLowerCase() + \".enabled\", \"true\");\n\n        properties.setProperty(\"simianarmy.chaos.ssh.key\", sshKey.getAbsolutePath());\n\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(properties);\n\n        ChaosMonkey chaos = new BasicChaosMonkey(ctx);\n        chaos.start();\n        chaos.stop();\n        return ctx;\n    }\n\n    private void checkSelected(TestChaosMonkeyContext ctx) {\n        List<InstanceGroup> selectedOn = ctx.selectedOn();\n        Assert.assertEquals(selectedOn.size(), 2);\n        Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(0).name(), \"name0\");\n        Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A);\n        Assert.assertEquals(selectedOn.get(1).name(), \"name1\");\n    }\n\n    private void checkNotifications(TestChaosMonkeyContext ctx, String key) {\n        List<Notification> notifications = ctx.getGloballyNotifiedList();\n        Assert.assertEquals(notifications.size(), 2);\n        Assert.assertEquals(notifications.get(0).getInstance(), \"0:i-123456789012345670\");\n        Assert.assertEquals(notifications.get(0).getChaosType().getKey(), key);\n        Assert.assertEquals(notifications.get(1).getInstance(), \"1:i-123456789012345671\");\n        Assert.assertEquals(notifications.get(1).getChaosType().getKey(), key);\n    }\n\n    private void checkSshActions(TestChaosMonkeyContext ctx, String key) {\n        List<SshAction> sshActions = ctx.getSshActions();\n        Assert.assertEquals(sshActions.size(), 4);\n\n        Assert.assertEquals(sshActions.get(0).getMethod(), \"put\");\n        Assert.assertEquals(sshActions.get(0).getInstanceId(), \"0:i-123456789012345670\");\n\n        // We require that each script include the name of the chaos type\n        // This makes testing easier, and also means the scripts show where they came from\n        Assert.assertTrue(sshActions.get(0).getContents().toLowerCase().contains(key.toLowerCase()));\n\n        Assert.assertEquals(sshActions.get(1).getMethod(), \"exec\");\n        Assert.assertEquals(sshActions.get(1).getInstanceId(), \"0:i-123456789012345670\");\n\n        Assert.assertEquals(sshActions.get(2).getMethod(), \"put\");\n        Assert.assertEquals(sshActions.get(2).getInstanceId(), \"1:i-123456789012345671\");\n        Assert.assertTrue(sshActions.get(2).getContents().contains(key));\n\n        Assert.assertEquals(sshActions.get(3).getMethod(), \"exec\");\n        Assert.assertEquals(sshActions.get(3).getInstanceId(), \"1:i-123456789012345671\");\n    }\n\n    @Test\n    public void testShutdownInstance() {\n        String key = \"ShutdownInstance\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n\n        checkNotifications(ctx, key);\n\n        List<String> terminated = ctx.terminated();\n        Assert.assertEquals(terminated.size(), 2);\n        Assert.assertEquals(terminated.get(0), \"0:i-123456789012345670\");\n        Assert.assertEquals(terminated.get(1), \"1:i-123456789012345671\");\n    }\n\n    @Test\n    public void testBlockAllNetworkTraffic() {\n        String key = \"BlockAllNetworkTraffic\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n\n        checkNotifications(ctx, key);\n\n        List<String> cloudActions = ctx.getCloudActions();\n        Assert.assertEquals(cloudActions.size(), 3);\n        Assert.assertEquals(cloudActions.get(0), \"createSecurityGroup:0:i-123456789012345670:blocked-network\");\n        Assert.assertEquals(cloudActions.get(1), \"setInstanceSecurityGroups:0:i-123456789012345670:sg-1\");\n        Assert.assertEquals(cloudActions.get(2), \"setInstanceSecurityGroups:1:i-123456789012345671:sg-1\");\n    }\n\n    @Test\n    public void testDetachVolumes() {\n        String key = \"DetachVolumes\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n\n        checkNotifications(ctx, key);\n\n        List<String> cloudActions = ctx.getCloudActions();\n        Assert.assertEquals(cloudActions.size(), 4);\n        Assert.assertEquals(cloudActions.get(0), \"detach:0:i-123456789012345670:volume-1\");\n        Assert.assertEquals(cloudActions.get(1), \"detach:0:i-123456789012345670:volume-2\");\n        Assert.assertEquals(cloudActions.get(2), \"detach:1:i-123456789012345671:volume-1\");\n        Assert.assertEquals(cloudActions.get(3), \"detach:1:i-123456789012345671:volume-2\");\n    }\n\n    @Test\n    public void testBurnCpu() {\n        String key = \"BurnCpu\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testBurnIo() {\n        String key = \"BurnIO\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testBurnIoWithoutBurnMoney() {\n        String key = \"BurnIO\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key, false);\n\n        checkSelected(ctx);\n\n        List<Notification> notifications = ctx.getGloballyNotifiedList();\n        Assert.assertEquals(notifications.size(), 0);\n\n        List<SshAction> sshActions = ctx.getSshActions();\n        Assert.assertEquals(sshActions.size(), 0);\n    }\n\n    @Test\n    public void testFillDisk() {\n        String key = \"FillDisk\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testFillDiskWithoutBurnMoney() {\n        String key = \"FillDisk\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key, false);\n\n        checkSelected(ctx);\n\n        List<Notification> notifications = ctx.getGloballyNotifiedList();\n        Assert.assertEquals(notifications.size(), 0);\n\n        List<SshAction> sshActions = ctx.getSshActions();\n        Assert.assertEquals(sshActions.size(), 0);\n    }\n\n\n    @Test\n    public void testFailDns() {\n        String key = \"FailDns\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testFailDynamoDb() {\n        String key = \"FailDynamoDb\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testFailEc2() {\n        String key = \"FailEc2\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testFailS3() {\n        String key = \"FailS3\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testKillProcess() {\n        String key = \"KillProcesses\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testNetworkCorruption() {\n        String key = \"NetworkCorruption\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testNetworkLatency() {\n        String key = \"NetworkLatency\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testNetworkLoss() {\n        String key = \"NetworkLoss\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n    @Test\n    public void testNullRoute() {\n        String key = \"NullRoute\";\n\n        TestChaosMonkeyContext ctx = runChaosMonkey(key);\n\n        checkSelected(ctx);\n        checkNotifications(ctx, key);\n        checkSshActions(ctx, key);\n    }\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/chaos/TestChaosMonkeyContext.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.chaos;\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.google.common.base.Joiner;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.CloudClient;\nimport com.netflix.simianarmy.GroupType;\nimport com.netflix.simianarmy.MonkeyConfiguration;\nimport com.netflix.simianarmy.TestMonkeyContext;\nimport com.netflix.simianarmy.basic.BasicConfiguration;\nimport com.netflix.simianarmy.basic.chaos.BasicChaosInstanceSelector;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport org.jclouds.compute.ComputeService;\nimport org.jclouds.compute.domain.ExecChannel;\nimport org.jclouds.compute.domain.ExecResponse;\nimport org.jclouds.domain.LoginCredentials;\nimport org.jclouds.io.Payload;\nimport org.jclouds.ssh.SshClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.InputStream;\nimport java.util.*;\n\npublic class TestChaosMonkeyContext extends TestMonkeyContext implements ChaosMonkey.Context {\n    private static final Logger LOGGER = LoggerFactory.getLogger(TestChaosMonkeyContext.class);\n    private final BasicConfiguration cfg;\n\n    public TestChaosMonkeyContext() {\n        this(new Properties());\n    }\n\n    protected TestChaosMonkeyContext(Properties properties) {\n        super(ChaosMonkey.Type.CHAOS);\n        cfg = new BasicConfiguration(properties);\n    }\n\n    public TestChaosMonkeyContext(String propFile) {\n        super(ChaosMonkey.Type.CHAOS);\n        Properties props = new Properties();\n        try {\n            InputStream is = TestChaosMonkeyContext.class.getResourceAsStream(propFile);\n            try {\n                props.load(is);\n            } finally {\n                is.close();\n            }\n        } catch (Exception e) {\n            LOGGER.error(\"Unable to load properties file \" + propFile, e);\n        }\n        cfg = new BasicConfiguration(props);\n    }\n\n    @Override\n    public MonkeyConfiguration configuration() {\n        return cfg;\n    }\n\n    public static class TestInstanceGroup implements InstanceGroup {\n        private final GroupType type;\n        private final String name;\n        private final String region;\n        private final List<String> instances = new ArrayList<String>();\n        private final List<TagDescription> tags = new ArrayList<TagDescription>();\n\n        public TestInstanceGroup(GroupType type, String name, String region, String... instances) {\n            this.type = type;\n            this.name = name;\n            this.region = region;\n            for (String i : instances) {\n                this.instances.add(i);\n            }\n        }\n\n        @Override\n        public List<TagDescription> tags() {\n            return tags;\n        }\n\n        @Override\n        public GroupType type() {\n            return type;\n        }\n\n        @Override\n        public String name() {\n            return name;\n        }\n\n        @Override\n        public String region() {\n            return region;\n        }\n\n        @Override\n        public List<String> instances() {\n            return Collections.unmodifiableList(instances);\n        }\n\n        @Override\n        public void addInstance(String ignored) {\n        }\n\n        public void deleteInstance(String id) {\n            instances.remove(id);\n        }\n\n        @Override\n        public InstanceGroup copyAs(String newName) {\n            return new TestInstanceGroup(this.type, newName, this.region, instances().toString());\n        }\n    }\n\n    public enum CrawlerTypes implements GroupType {\n        TYPE_A, TYPE_B, TYPE_C, TYPE_D\n    };\n\n    @Override\n    public ChaosCrawler chaosCrawler() {\n        return new ChaosCrawler() {\n            @Override\n            public EnumSet<?> groupTypes() {\n                return EnumSet.allOf(CrawlerTypes.class);\n            }\n\n            @Override\n            public List<InstanceGroup> groups() {\n                InstanceGroup gA0 = new TestInstanceGroup(CrawlerTypes.TYPE_A, \"name0\", \"reg1\", \"0:i-123456789012345670\");\n                InstanceGroup gA1 = new TestInstanceGroup(CrawlerTypes.TYPE_A, \"name1\", \"reg1\", \"1:i-123456789012345671\");\n                InstanceGroup gB2 = new TestInstanceGroup(CrawlerTypes.TYPE_B, \"name2\", \"reg1\", \"2:i-123456789012345672\");\n                InstanceGroup gB3 = new TestInstanceGroup(CrawlerTypes.TYPE_B, \"name3\", \"reg1\", \"3:i-123456789012345673\");\n                InstanceGroup gC1 = new TestInstanceGroup(CrawlerTypes.TYPE_C, \"name4\", \"reg1\", \"3:i-123456789012345674\",\n                        \"3:i-123456789012345675\");\n                InstanceGroup gC2 = new TestInstanceGroup(CrawlerTypes.TYPE_C, \"name5\", \"reg1\", \"3:i-123456789012345676\",\n                        \"3:i-123456789012345677\");\n                InstanceGroup gD0 = new TestInstanceGroup(CrawlerTypes.TYPE_D, \"new-group-TestGroup1-XXXXXXXXX\",\n                        \"reg1\", \"3:i-123456789012345678\", \"3:i-123456789012345679\");\n                return Arrays.asList(gA0, gA1, gB2, gB3, gC1, gC2, gD0);\n            }\n\n            @Override\n            public List<InstanceGroup> groups(String... names) {\n                Map<String, InstanceGroup> nameToGroup = new HashMap<String, InstanceGroup>();\n                for (InstanceGroup ig : groups()) {\n                    nameToGroup.put(ig.name(), ig);\n                }\n                List<InstanceGroup> list = new LinkedList<InstanceGroup>();\n                for (String name : names) {\n                    InstanceGroup ig = nameToGroup.get(name);\n                    if (ig == null) {\n                        continue;\n                    }\n                    for (String instanceId : selected) {\n                        // Remove selected instances from crawler list\n                        TestInstanceGroup testIg = (TestInstanceGroup) ig;\n                        testIg.deleteInstance(instanceId);\n                    }\n                    list.add(ig);\n                }\n                return list;\n            }\n        };\n    }\n\n    private final List<InstanceGroup> selectedOn = new LinkedList<InstanceGroup>();\n\n    public List<InstanceGroup> selectedOn() {\n        return selectedOn;\n    }\n\n    @Override\n    public ChaosInstanceSelector chaosInstanceSelector() {\n        return new BasicChaosInstanceSelector() {\n            @Override\n            public Collection<String> select(InstanceGroup group, double probability) {\n                selectedOn.add(group);\n                Collection<String> instances = super.select(group, probability);\n                selected.addAll(instances);\n                return instances;\n            }\n        };\n    }\n\n    private final List<String> terminated = new LinkedList<String>();\n    private final List<String> selected = Lists.newArrayList();\n    private final List<String> cloudActions = Lists.newArrayList();\n\n    public List<String> terminated() {\n        return terminated;\n    }\n\n    private final Map<String, String> securityGroupNames = Maps.newHashMap();\n\n    @Override\n    public CloudClient cloudClient() {\n        return new CloudClient() {\n            @Override\n            public void terminateInstance(String instanceId) {\n                terminated.add(instanceId);\n            }\n\n            @Override\n            public void createTagsForResources(Map<String, String> keyValueMap, String... resourceIds) {\n            }\n\n            @Override\n            public void deleteAutoScalingGroup(String asgName) {\n            }\n\n            @Override\n            public void deleteVolume(String volumeId) {\n            }\n\n            @Override\n            public void deleteSnapshot(String snapshotId) {\n            }\n\n            @Override\n            public void deleteImage(String imageId) {\n            }\n\n            @Override\n            public void deleteLaunchConfiguration(String launchConfigName) {\n            }\n\n            @Override\n            public void deleteElasticLoadBalancer(String elbId) {\n            }\n\n            @Override\n            public void deleteDNSRecord(String dnsname, String dnstype, String hostedzoneid) {\n            }\n\n            @Override\n            public List<String> listAttachedVolumes(String instanceId, boolean includeRoot) {\n                List<String> volumes = Lists.newArrayList();\n                if (includeRoot) {\n                    volumes.add(\"volume-0\");\n                }\n                volumes.add(\"volume-1\");\n                volumes.add(\"volume-2\");\n                return volumes;\n            }\n\n            @Override\n            public void detachVolume(String instanceId, String volumeId, boolean force) {\n                cloudActions.add(\"detach:\" + instanceId + \":\" + volumeId);\n            }\n\n            @Override\n            public ComputeService getJcloudsComputeService() {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public String getJcloudsId(String instanceId) {\n                throw new UnsupportedOperationException();\n            }\n\n            @Override\n            public SshClient connectSsh(String instanceId, LoginCredentials credentials) {\n                return new MockSshClient(instanceId, credentials);\n            }\n\n            @Override\n            public String findSecurityGroup(String instanceId, String groupName) {\n                return securityGroupNames.get(groupName);\n            }\n\n            @Override\n            public String createSecurityGroup(String instanceId, String groupName, String description) {\n                String id = \"sg-\" + (securityGroupNames.size() + 1);\n                securityGroupNames.put(groupName, id);\n                cloudActions.add(\"createSecurityGroup:\" + instanceId + \":\" + groupName);\n                return id;\n            }\n\n            @Override\n            public boolean canChangeInstanceSecurityGroups(String instanceId) {\n                return true;\n            }\n\n            @Override\n            public void setInstanceSecurityGroups(String instanceId, List<String> groupIds) {\n                cloudActions.add(\"setInstanceSecurityGroups:\" + instanceId + \":\" + Joiner.on(',').join(groupIds));\n            }\n        };\n    }\n\n    private final List<SshAction> sshActions = Lists.newArrayList();\n\n    public static class SshAction {\n        private String instanceId;\n        private String method;\n        private String path;\n        private String contents;\n        private String command;\n\n        public String getInstanceId() {\n            return instanceId;\n        }\n\n        public String getMethod() {\n            return method;\n        }\n\n        public String getPath() {\n            return path;\n        }\n\n        public String getContents() {\n            return contents;\n        }\n\n        public String getCommand() {\n            return command;\n        }\n    }\n\n    private class MockSshClient implements SshClient {\n        private final String instanceId;\n        private final LoginCredentials credentials;\n\n        public MockSshClient(String instanceId, LoginCredentials credentials) {\n            this.instanceId = instanceId;\n            this.credentials = credentials;\n        }\n\n        @Override\n        public String getUsername() {\n            return credentials.getUser();\n        }\n\n        @Override\n        public String getHostAddress() {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public void put(String path, Payload contents) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Payload get(String path) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public ExecResponse exec(String command) {\n            SshAction action = new SshAction();\n            action.method = \"exec\";\n            action.instanceId = instanceId;\n            action.command = command;\n            sshActions.add(action);\n\n            String output = \"\";\n            String error = \"\";\n            int exitStatus = 0;\n            return new ExecResponse(output, error, exitStatus);\n        }\n\n        @Override\n        public ExecChannel execChannel(String command) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public void connect() {\n        }\n\n        @Override\n        public void disconnect() {\n        }\n\n        @Override\n        public void put(String path, String contents) {\n            SshAction action = new SshAction();\n            action.method = \"put\";\n            action.instanceId = instanceId;\n            action.path = path;\n            action.contents = contents;\n            sshActions.add(action);\n        }\n    }\n\n    private List<Notification> groupNotified = Lists.newArrayList();\n    private List<Notification> globallyNotified = Lists.newArrayList();\n\n    static class Notification {\n        private final String instance;\n        private final ChaosType chaosType;\n\n        public Notification(String instance, ChaosType chaosType) {\n            this.instance = instance;\n            this.chaosType = chaosType;\n        }\n\n        public String getInstance() {\n            return instance;\n        }\n\n        public ChaosType getChaosType() {\n            return chaosType;\n        }\n    }\n\n    @Override\n    public ChaosEmailNotifier chaosEmailNotifier() {\n        return new ChaosEmailNotifier(null) {\n            @Override\n            public String getSourceAddress(String to) {\n                return \"source@chaosMonkey.foo\";\n            }\n\n            @Override\n            public String[] getCcAddresses(String to) {\n                return new String[] {};\n            }\n\n            @Override\n            public String buildEmailSubject(String to) {\n                return String.format(\"Testing Chaos termination notification for %s\", to);\n            }\n\n            @Override\n            public void sendTerminationNotification(InstanceGroup group, String instance, ChaosType chaosType) {\n                groupNotified.add(new Notification(instance, chaosType));\n            }\n\n            @Override\n            public void sendTerminationGlobalNotification(InstanceGroup group, String instance, ChaosType chaosType) {\n                globallyNotified.add(new Notification(instance, chaosType));\n            }\n        };\n    }\n\n    public int getNotified() {\n        return groupNotified.size();\n    }\n\n    public int getGloballyNotified() {\n        return globallyNotified.size();\n    }\n\n    public List<Notification> getNotifiedList() {\n        return groupNotified;\n    }\n\n    public List<Notification> getGloballyNotifiedList() {\n        return globallyNotified;\n    }\n\n    public List<SshAction> getSshActions() {\n        return sshActions;\n    }\n\n    public List<String> getCloudActions() {\n        return cloudActions;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/client/aws/TestAWSClient.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.client.aws;\n\nimport com.amazonaws.services.autoscaling.AmazonAutoScalingClient;\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.DescribeAutoScalingGroupsRequest;\nimport com.amazonaws.services.autoscaling.model.DescribeAutoScalingGroupsResult;\nimport com.amazonaws.services.autoscaling.model.Instance;\nimport com.amazonaws.services.ec2.AmazonEC2;\nimport com.amazonaws.services.ec2.model.TerminateInstancesRequest;\nimport org.mockito.ArgumentCaptor;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\npublic class TestAWSClient extends AWSClient {\n    public TestAWSClient() {\n        super(\"us-east-1\");\n    }\n\n    private AmazonEC2 ec2Mock = mock(AmazonEC2.class);\n\n    protected AmazonEC2 ec2Client() {\n        return ec2Mock;\n    }\n\n    private AmazonAutoScalingClient asgMock = mock(AmazonAutoScalingClient.class);\n\n    protected AmazonAutoScalingClient asgClient() {\n        return asgMock;\n    }\n\n    protected AmazonEC2 superEc2Client() {\n        return super.ec2Client();\n    }\n\n    protected AmazonAutoScalingClient superAsgClient() {\n        return super.asgClient();\n    }\n\n    @Test\n    public void testClients() {\n        TestAWSClient client1 = new TestAWSClient();\n        Assert.assertNotNull(client1.superEc2Client(), \"non null super ec2Client\");\n        Assert.assertNotNull(client1.superAsgClient(), \"non null super asgClient\");\n    }\n\n    @Test\n    public void testTerminateInstance() {\n\n        ArgumentCaptor<TerminateInstancesRequest> arg = ArgumentCaptor.forClass(TerminateInstancesRequest.class);\n\n        this.terminateInstance(\"fake:i-12345678901234567\");\n\n        verify(ec2Mock).terminateInstances(arg.capture());\n\n        List<String> instances = arg.getValue().getInstanceIds();\n        Assert.assertEquals(instances.size(), 1);\n        Assert.assertEquals(instances.get(0), \"fake:i-12345678901234567\");\n    }\n\n    private DescribeAutoScalingGroupsResult mkAsgResult(String asgName, String instanceId) {\n        DescribeAutoScalingGroupsResult result = new DescribeAutoScalingGroupsResult();\n        AutoScalingGroup asg = new AutoScalingGroup();\n        asg.setAutoScalingGroupName(asgName);\n        Instance inst = new Instance();\n        inst.setInstanceId(instanceId);\n        asg.setInstances(Arrays.asList(inst));\n        result.setAutoScalingGroups(Arrays.asList(asg));\n        return result;\n    }\n\n    @Test\n    public void testDescribeAutoScalingGroups() {\n        DescribeAutoScalingGroupsResult result1 = mkAsgResult(\"asg1\", \"i-123456789012345670\");\n        result1.setNextToken(\"nextToken\");\n        DescribeAutoScalingGroupsResult result2 = mkAsgResult(\"asg2\", \"i-123456789012345671\");\n\n        when(asgMock.describeAutoScalingGroups(any(DescribeAutoScalingGroupsRequest.class))).thenReturn(result1)\n                .thenReturn(result2);\n\n        List<AutoScalingGroup> asgs = this.describeAutoScalingGroups();\n\n        verify(asgMock, times(2)).describeAutoScalingGroups(any(DescribeAutoScalingGroupsRequest.class));\n\n        Assert.assertEquals(asgs.size(), 2);\n\n        // 2 asgs with 1 instance each\n        Assert.assertEquals(asgs.get(0).getAutoScalingGroupName(), \"asg1\");\n        Assert.assertEquals(asgs.get(0).getInstances().size(), 1);\n        Assert.assertEquals(asgs.get(0).getInstances().get(0).getInstanceId(), \"i-123456789012345670\");\n\n        Assert.assertEquals(asgs.get(1).getAutoScalingGroupName(), \"asg2\");\n        Assert.assertEquals(asgs.get(1).getInstances().size(), 1);\n        Assert.assertEquals(asgs.get(1).getInstances().get(0).getInstanceId(), \"i-123456789012345671\");\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/client/aws/chaos/TestASGChaosCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.client.aws.chaos;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Arrays;\nimport java.util.EnumSet;\nimport java.util.HashSet;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Set;\n\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\nimport com.amazonaws.services.autoscaling.model.Instance;\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.netflix.simianarmy.basic.chaos.BasicInstanceGroup;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport com.netflix.simianarmy.client.aws.AWSClient;\nimport com.netflix.simianarmy.tunable.TunableInstanceGroup;\n\npublic class TestASGChaosCrawler {\n    private final ASGChaosCrawler crawler;\n\n    private AutoScalingGroup mkAsg(String asgName, String instanceId) {\n        AutoScalingGroup asg = new AutoScalingGroup();\n        asg.setAutoScalingGroupName(asgName);\n        Instance inst = new Instance();\n        inst.setInstanceId(instanceId);\n        asg.setInstances(Arrays.asList(inst));\n        return asg;\n    }\n\n    private final AWSClient awsMock;\n\n    public TestASGChaosCrawler() {\n        awsMock = mock(AWSClient.class);\n        crawler = new ASGChaosCrawler(awsMock);\n    }\n\n    @Test\n    public void testGroupTypes() {\n        EnumSet<?> types = crawler.groupTypes();\n        Assert.assertEquals(types.size(), 1);\n        Assert.assertEquals(types.iterator().next().name(), \"ASG\");\n    }\n\n    @Test\n    public void testGroups() {\n        List<AutoScalingGroup> asgList = new LinkedList<AutoScalingGroup>();\n        asgList.add(mkAsg(\"asg1\", \"i-123456789012345670\"));\n        asgList.add(mkAsg(\"asg2\", \"i-123456789012345671\"));\n\n        when(awsMock.describeAutoScalingGroups((String[]) null)).thenReturn(asgList);\n\n        List<InstanceGroup> groups = crawler.groups();\n\n        verify(awsMock, times(1)).describeAutoScalingGroups((String[]) null);\n\n        Assert.assertEquals(groups.size(), 2);\n\n        Assert.assertEquals(groups.get(0).type(), ASGChaosCrawler.Types.ASG);\n        Assert.assertEquals(groups.get(0).name(), \"asg1\");\n        Assert.assertEquals(groups.get(0).instances().size(), 1);\n        Assert.assertEquals(groups.get(0).instances().get(0), \"i-123456789012345670\");\n\n        Assert.assertEquals(groups.get(1).type(), ASGChaosCrawler.Types.ASG);\n        Assert.assertEquals(groups.get(1).name(), \"asg2\");\n        Assert.assertEquals(groups.get(1).instances().size(), 1);\n        Assert.assertEquals(groups.get(1).instances().get(0), \"i-123456789012345671\");\n    }\n    \n    @Test\n    public void testFindAggressionCoefficient() {\n      AutoScalingGroup asg1 = mkAsg(\"asg1\", \"i-123456789012345670\");\n      Set<TagDescription> tagDescriptions = new HashSet<>();\n      tagDescriptions.add(makeTunableTag(\"1.0\"));\n      asg1.setTags(tagDescriptions);\n      \n      double aggression = crawler.findAggressionCoefficient(asg1);\n      \n      Assert.assertEquals(aggression, 1.0);\n    }\n    \n    @Test\n    public void testFindAggressionCoefficient_two() {\n      AutoScalingGroup asg1 = mkAsg(\"asg1\", \"i-123456789012345670\");\n      Set<TagDescription> tagDescriptions = new HashSet<>();\n      tagDescriptions.add(makeTunableTag(\"2.0\"));\n      asg1.setTags(tagDescriptions);\n      \n      double aggression = crawler.findAggressionCoefficient(asg1);\n      \n      Assert.assertEquals(aggression, 2.0);\n    }\n    \n    @Test\n    public void testFindAggressionCoefficient_null() {\n      AutoScalingGroup asg1 = mkAsg(\"asg1\", \"i-123456789012345670\");\n      Set<TagDescription> tagDescriptions = new HashSet<>();\n      tagDescriptions.add(makeTunableTag(null));\n      asg1.setTags(tagDescriptions);\n      \n      double aggression = crawler.findAggressionCoefficient(asg1);\n      \n      Assert.assertEquals(aggression, 1.0);\n    }\n\n    @Test\n    public void testFindAggressionCoefficient_unparsable() {\n      AutoScalingGroup asg1 = mkAsg(\"asg1\", \"i-123456789012345670\");\n      Set<TagDescription> tagDescriptions = new HashSet<>();\n      tagDescriptions.add(makeTunableTag(\"not a number\"));\n      asg1.setTags(tagDescriptions);\n      \n      double aggression = crawler.findAggressionCoefficient(asg1);\n      \n      Assert.assertEquals(aggression, 1.0);\n    }\n\n    private TagDescription makeTunableTag(String value) {\n      TagDescription desc = new TagDescription();\n      desc.setKey(\"chaosMonkey.aggressionCoefficient\");\n      desc.setValue(value);\n      return desc;\n    }\n    \n    @Test \n    public void testGetInstanceGroup_basic() {\n      AutoScalingGroup asg = mkAsg(\"asg1\", \"i-123456789012345670\");\n\n      InstanceGroup group = crawler.getInstanceGroup(asg, 1.0);\n      \n      Assert.assertTrue( (group instanceof BasicInstanceGroup) );\n      Assert.assertFalse( (group instanceof TunableInstanceGroup) );\n    }\n\n    @Test \n    public void testGetInstanceGroup_tunable() {\n      AutoScalingGroup asg = mkAsg(\"asg1\", \"i-123456789012345670\");\n\n      InstanceGroup group = crawler.getInstanceGroup(asg, 2.0);\n      \n      Assert.assertTrue( (group instanceof TunableInstanceGroup) );\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/client/aws/chaos/TestFilterASGChaosCrawler.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.client.aws.chaos;\n\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport com.netflix.simianarmy.GroupType;\nimport com.netflix.simianarmy.basic.chaos.BasicInstanceGroup;\nimport com.netflix.simianarmy.chaos.ChaosCrawler;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\n\nimport org.testng.annotations.BeforeTest;\nimport org.testng.annotations.Test;\n\n\nimport java.util.*;\n\nimport static org.mockito.Mockito.*;\nimport static org.testng.Assert.assertEquals;\n\npublic class TestFilterASGChaosCrawler {\n\n    private ChaosCrawler crawlerMock;\n    private ChaosCrawler crawler;\n    private String tagKey, tagValue;\n\n    public enum Types implements GroupType {\n\n        /** only crawls AutoScalingGroups. */\n        ASG;\n    }\n\n    @BeforeTest\n    public void beforeTest() {\n        crawlerMock = mock(ChaosCrawler.class);\n        tagKey = \"key-\" + UUID.randomUUID().toString();\n        tagValue = \"tagValue-\" + UUID.randomUUID().toString();\n        crawler = new FilteringChaosCrawler(crawlerMock, new TagPredicate(tagKey, tagValue));\n    }\n\n    @Test\n    public void testFilterGroups() {\n\n        List<TagDescription> tagList = new ArrayList<TagDescription>();\n        TagDescription td = new TagDescription();\n        td.setKey(tagKey);\n        td.setValue(tagValue);\n        tagList.add(td);\n\n        List<InstanceGroup> listGroup = new LinkedList<InstanceGroup>();\n        listGroup.add(new BasicInstanceGroup(\"asg1\", Types.ASG, \"region1\", tagList) );\n        listGroup.add(new BasicInstanceGroup(\"asg2\", Types.ASG, \"region2\", Collections.<TagDescription>emptyList()) );\n        listGroup.add(new BasicInstanceGroup(\"asg3\", Types.ASG, \"region3\", tagList) );\n        listGroup.add(new BasicInstanceGroup(\"asg4\", Types.ASG, \"region4\", Collections.<TagDescription>emptyList()) );\n\n        when(crawlerMock.groups()).thenReturn(listGroup);\n        List<InstanceGroup> groups = crawlerMock.groups();\n\n        assertEquals(groups.size(), 4);\n\n        groups = crawler.groups();\n\n\n\n        assertEquals(groups.size(), 2);\n\n        assertEquals(groups.get(0).name(), \"asg1\");\n\n        assertEquals(groups.get(1).name(), \"asg3\");\n    }\n\n\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/client/vsphere/TestPropertyBasedTerminationStrategy.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n//CHECKSTYLE IGNORE Javadoc\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport static org.mockito.Mockito.mock;\r\nimport static org.mockito.Mockito.times;\r\nimport static org.mockito.Mockito.verify;\r\nimport static org.mockito.Mockito.when;\r\nimport static org.testng.Assert.assertEquals;\r\nimport static org.testng.Assert.fail;\r\n\r\nimport java.rmi.RemoteException;\r\n\r\nimport org.testng.annotations.Test;\r\n\r\nimport com.netflix.simianarmy.basic.BasicConfiguration;\r\nimport com.vmware.vim25.mo.VirtualMachine;\r\n\r\n/**\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class TestPropertyBasedTerminationStrategy {\r\n    private BasicConfiguration configMock = mock(BasicConfiguration.class);\r\n    private VirtualMachine virtualMachineMock = mock(VirtualMachine.class);\r\n\r\n    @Test\r\n    public void shouldReturnConfiguredPropertyNameAndValueAfterConstructedFromConfig() {\r\n        when(configMock.getStrOrElse(\"simianarmy.client.vsphere.terminationStrategy.property.name\", \"Force Boot\"))\r\n            .thenReturn(\"configured name\");\r\n        when(configMock.getStrOrElse(\"simianarmy.client.vsphere.terminationStrategy.property.value\", \"server\"))\r\n            .thenReturn(\"configured value\");\r\n\r\n        PropertyBasedTerminationStrategy strategy = new PropertyBasedTerminationStrategy(configMock);\r\n\r\n        assertEquals(strategy.getPropertyName(), \"configured name\");\r\n        assertEquals(strategy.getPropertyValue(), \"configured value\");\r\n    }\r\n\r\n    @Test\r\n    public void shouldSetPropertyAndResetVirtualMachineAfterTermination() {\r\n        when(configMock.getStrOrElse(\"simianarmy.client.vsphere.terminationStrategy.property.name\", \"Force Boot\"))\r\n            .thenReturn(\"configured name\");\r\n        when(configMock.getStrOrElse(\"simianarmy.client.vsphere.terminationStrategy.property.value\", \"server\"))\r\n            .thenReturn(\"configured value\");\r\n\r\n        PropertyBasedTerminationStrategy strategy = new PropertyBasedTerminationStrategy(configMock);\r\n\r\n        try {\r\n            strategy.terminate(virtualMachineMock);\r\n            verify(virtualMachineMock, times(1)).setCustomValue(\"configured name\", \"configured value\");\r\n            verify(virtualMachineMock, times(1)).resetVM_Task();\r\n        } catch (RemoteException e) {\r\n            fail(\"termination should not fail\", e);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/client/vsphere/TestVSpehereClient.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n// CHECKSTYLE IGNORE Javadoc\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport static org.mockito.Mockito.mock;\r\nimport static org.mockito.Mockito.when;\r\nimport static org.testng.Assert.assertTrue;\r\nimport static org.mockito.Mockito.times;\r\nimport static org.mockito.Mockito.verify;\r\n\r\nimport java.rmi.RemoteException;\r\nimport java.util.List;\r\n\r\nimport org.testng.annotations.Test;\r\n\r\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\r\nimport com.amazonaws.services.autoscaling.model.Instance;\r\nimport com.vmware.vim25.mo.ManagedEntity;\r\nimport com.vmware.vim25.mo.VirtualMachine;\r\n\r\n/**\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class TestVSpehereClient {\r\n    @Test\r\n    public void shouldTerminateCorrectly() throws RemoteException {\r\n        VSphereServiceConnection connection = mock(VSphereServiceConnection.class);\r\n        VirtualMachine vm1 = createVMMock(\"vm1\");\r\n        when(connection.getVirtualMachineById(\"vm1\")).thenReturn(vm1);\r\n\r\n        TerminationStrategy strategy = mock(PropertyBasedTerminationStrategy.class);\r\n\r\n        VSphereClient client = new VSphereClient(strategy, connection);\r\n        client.terminateInstance(\"vm1\");\r\n\r\n        verify(strategy, times(1)).terminate(vm1);\r\n    }\r\n\r\n    @Test\r\n    public void shouldDescribeGroupsCorrectly() {\r\n        VSphereServiceConnection connection = mock(VSphereServiceConnection.class);\r\n        TerminationStrategy strategy = mock(PropertyBasedTerminationStrategy.class);\r\n        VirtualMachine[] virtualMachines = {createVMMock(\"vm1\"), createVMMock(\"vm2\")};\r\n        when(connection.describeVirtualMachines()).thenReturn(virtualMachines);\r\n\r\n        VSphereClient client = new VSphereClient(strategy, connection);\r\n\r\n        List<AutoScalingGroup> groups = client.describeAutoScalingGroups();\r\n        String str = flattenGroups(groups);\r\n\r\n        assertTrue(groups.size() == 2, \"did not desribes the 2 vm's that were given\");\r\n        assertTrue(str.indexOf(\"group:vm1.parent.name:id:vm1.name:\") >= 0, \"did not describe vm1 correctly\");\r\n        assertTrue(str.indexOf(\"group:vm2.parent.name:id:vm2.name:\") >= 0, \"did not describe vm2 correctly\");\r\n    }\r\n\r\n    private String flattenGroups(List<AutoScalingGroup> groups) {\r\n        StringBuilder buf = new StringBuilder();\r\n        for (AutoScalingGroup asg : groups) {\r\n            List<Instance> instances = asg.getInstances();\r\n            buf.append(\"group:\").append(asg.getAutoScalingGroupName()).append(\":\");\r\n            for (Instance instance : instances) {\r\n                buf.append(\"id:\").append(instance.getInstanceId()).append(\":\");\r\n            }\r\n        }\r\n        return buf.toString();\r\n    }\r\n\r\n    private VirtualMachine createVMMock(String id) {\r\n        VirtualMachine vm1 = mock(VirtualMachine.class);\r\n        ManagedEntity me1 = mock(ManagedEntity.class);\r\n        when(vm1.getName()).thenReturn(id + \".name\");\r\n        when(vm1.getParent()).thenReturn(me1);\r\n        when(me1.getName()).thenReturn(id + \".parent.name\");\r\n        return vm1;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/client/vsphere/TestVSphereContext.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n// CHECKSTYLE IGNORE Javadoc\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport static org.testng.Assert.assertNotNull;\r\nimport static org.testng.Assert.assertTrue;\r\n\r\nimport org.testng.annotations.Test;\r\n\r\nimport com.netflix.simianarmy.client.aws.AWSClient;\r\n\r\n/**\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class TestVSphereContext {\r\n    @Test\r\n    public void shouldSetClientOfCorrectType() {\r\n        VSphereContext context = new VSphereContext();\r\n        AWSClient awsClient = context.awsClient();\r\n        assertNotNull(awsClient);\r\n        assertTrue(awsClient instanceof VSphereClient);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/client/vsphere/TestVSphereGroups.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n// CHECKSTYLE IGNORE Javadoc\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport static org.testng.Assert.assertEquals;\r\n\r\nimport java.util.List;\r\n\r\nimport org.testng.annotations.Test;\r\n\r\nimport com.amazonaws.services.autoscaling.model.AutoScalingGroup;\r\nimport com.amazonaws.services.autoscaling.model.Instance;\r\n/**\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class TestVSphereGroups {\r\n    @Test\r\n    public void shouldReturnListContainigSingleASGWhenAddInstanceIsCalledOnce() {\r\n        VSphereGroups groups = new VSphereGroups();\r\n        groups.addInstance(\"anyInstanceId\", \"anyGroupName\");\r\n        List<AutoScalingGroup> list = groups.asList();\r\n\r\n        assertEquals(1, list.size());\r\n\r\n        AutoScalingGroup firstItem = list.get(0);\r\n        assertEquals(\"anyGroupName\", firstItem.getAutoScalingGroupName());\r\n\r\n        List<Instance> instances = firstItem.getInstances();\r\n        assertEquals(1, instances.size());\r\n\r\n        assertEquals(\"anyInstanceId\", instances.get(0).getInstanceId());\r\n    }\r\n\r\n    @Test\r\n    public void shouldReturnListContainingSingleASGWithTwoInstancesWhenAddInstanceIsCaledTwiceForSameGroup() {\r\n        VSphereGroups groups = new VSphereGroups();\r\n        groups.addInstance(\"anyInstanceId\", \"anyGroupName\");\r\n        groups.addInstance(\"anyOtherInstanceId\", \"anyGroupName\");\r\n        List<AutoScalingGroup> list = groups.asList();\r\n\r\n        assertEquals(1, list.size());\r\n\r\n        List<Instance> instances = list.get(0).getInstances();\r\n        assertEquals(2, instances.size());\r\n\r\n        assertEquals(\"anyInstanceId\", instances.get(0).getInstanceId());\r\n        assertEquals(\"anyOtherInstanceId\", instances.get(1).getInstanceId());\r\n    }\r\n\r\n    @Test\r\n    public void shouldReturnListContainigTwoASGWhenAddInstanceIsCalledTwice() {\r\n        VSphereGroups groups = new VSphereGroups();\r\n        groups.addInstance(\"anyInstanceId\", \"anyGroupName\");\r\n        groups.addInstance(\"anyOtherInstanceId\", \"anyOtherGroupName\");\r\n        List<AutoScalingGroup> list = groups.asList();\r\n\r\n        assertEquals(2, list.size());\r\n\r\n        AutoScalingGroup firstGroup = list.get(0);\r\n        assertEquals(\"anyGroupName\", firstGroup.getAutoScalingGroupName());\r\n\r\n        List<Instance> firstGroupInstances = firstGroup.getInstances();\r\n        assertEquals(1, firstGroupInstances.size());\r\n\r\n        assertEquals(\"anyInstanceId\", firstGroupInstances.get(0).getInstanceId());\r\n\r\n        AutoScalingGroup secondGroup = list.get(1);\r\n        assertEquals(\"anyOtherGroupName\", secondGroup.getAutoScalingGroupName());\r\n\r\n        List<Instance> secondGroupInstances = secondGroup.getInstances();\r\n        assertEquals(1, secondGroupInstances.size());\r\n\r\n        assertEquals(\"anyOtherInstanceId\", secondGroupInstances.get(0).getInstanceId());\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/client/vsphere/TestVSphereServiceConnection.java",
    "content": "/*\r\n *  Copyright 2012 Immobilien Scout GmbH\r\n *\r\n * Licensed under the Apache License, Version 2.0 (the \"License\");\r\n * you may not use this file except in compliance with the License.\r\n * You may obtain a copy of the License at\r\n *\r\n *     http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS,\r\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n * See the License for the specific language governing permissions and\r\n * limitations under the License.\r\n */\r\n// CHECKSTYLE IGNORE Javadoc\r\npackage com.netflix.simianarmy.client.vsphere;\r\n\r\nimport static com.netflix.simianarmy.client.vsphere.VSphereServiceConnection.VIRTUAL_MACHINE_TYPE_NAME;\r\nimport static junit.framework.Assert.assertSame;\r\nimport static org.mockito.Mockito.mock;\r\nimport static org.mockito.Mockito.verify;\r\nimport static org.mockito.Mockito.when;\r\nimport static org.testng.Assert.assertEquals;\r\n\r\nimport java.rmi.RemoteException;\r\n\r\nimport org.testng.Assert;\r\nimport org.testng.annotations.Test;\r\n\r\nimport com.amazonaws.AmazonServiceException;\r\nimport com.netflix.simianarmy.basic.BasicConfiguration;\r\nimport com.vmware.vim25.InvalidProperty;\r\nimport com.vmware.vim25.RuntimeFault;\r\nimport com.vmware.vim25.mo.InventoryNavigator;\r\nimport com.vmware.vim25.mo.ManagedEntity;\r\nimport com.vmware.vim25.mo.VirtualMachine;\r\n\r\n/**\r\n * @author ingmar.krusch@immobilienscout24.de\r\n */\r\npublic class TestVSphereServiceConnection {\r\n    // private ServiceInstance serviceMock = mock(ServiceInstance.class);\r\n    private BasicConfiguration configMock = mock(BasicConfiguration.class);\r\n\r\n    @Test\r\n    public void shouldReturnConfiguredPropertiesAfterConstructedFromConfig() {\r\n        when(configMock.getStr(\"simianarmy.client.vsphere.username\")).thenReturn(\"configured username\");\r\n        when(configMock.getStr(\"simianarmy.client.vsphere.password\")).thenReturn(\"configured password\");\r\n        when(configMock.getStr(\"simianarmy.client.vsphere.url\")).thenReturn(\"configured url\");\r\n\r\n        VSphereServiceConnection service = new VSphereServiceConnection(configMock);\r\n\r\n        assertEquals(service.getUsername(), \"configured username\");\r\n        assertEquals(service.getPassword(), \"configured password\");\r\n        assertEquals(service.getUrl(), \"configured url\");\r\n    }\r\n\r\n    @Test\r\n    public void shouldCallSearchManagedEntityAndReturnVMForDoItGetVirtualMachineById()\r\n                    throws RemoteException {\r\n        VSphereServiceConnectionWithMockedInventoryNavigator service =\r\n                        new VSphereServiceConnectionWithMockedInventoryNavigator();\r\n        InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock();\r\n\r\n        VirtualMachine vmMock = mock(VirtualMachine.class);\r\n        when(inventoryNavigatorMock.searchManagedEntity(VIRTUAL_MACHINE_TYPE_NAME, \"instanceId\")).thenReturn(vmMock);\r\n\r\n        VirtualMachine actualVM = service.getVirtualMachineById(\"instanceId\");\r\n\r\n        verify(inventoryNavigatorMock).searchManagedEntity(VIRTUAL_MACHINE_TYPE_NAME, \"instanceId\");\r\n        assertSame(vmMock, actualVM);\r\n    }\r\n\r\n    @Test //(expectedExceptions = AmazonServiceException.class)\r\n    public void shouldThrowExceptionWhenCallingSearchManagedEntitiesOnDescribeWhenNoVMsAreReturned()\r\n                    throws RemoteException {\r\n        VSphereServiceConnectionWithMockedInventoryNavigator service =\r\n                        new VSphereServiceConnectionWithMockedInventoryNavigator();\r\n\r\n        try {\r\n            service.describeVirtualMachines();\r\n        } catch (AmazonServiceException e) {\r\n            Assert.assertTrue(e != null);\r\n        }\r\n    }\r\n\r\n    @Test\r\n    public void shouldCallSearchManagedEntitiesOnDescribeWhenAtLeastOneVMIsReturned()\r\n                    throws RemoteException {\r\n        VSphereServiceConnectionWithMockedInventoryNavigator service =\r\n                        new VSphereServiceConnectionWithMockedInventoryNavigator();\r\n        InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock();\r\n\r\n        ManagedEntity[] meMocks = new ManagedEntity[] {mock(VirtualMachine.class)};\r\n        when(inventoryNavigatorMock.searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME)).thenReturn(meMocks);\r\n\r\n        VirtualMachine[] actualVMs = service.describeVirtualMachines();\r\n\r\n        verify(inventoryNavigatorMock).searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME);\r\n        assertSame(meMocks[0], actualVMs[0]);\r\n    }\r\n\r\n    @Test(expectedExceptions = AmazonServiceException.class)\r\n    public void shouldEncapsulateInvalidPropertyException() throws RemoteException {\r\n        VSphereServiceConnectionWithMockedInventoryNavigator service =\r\n                        new VSphereServiceConnectionWithMockedInventoryNavigator();\r\n        InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock();\r\n        when(inventoryNavigatorMock.searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME)).thenThrow(new InvalidProperty());\r\n\r\n        service.describeVirtualMachines();\r\n    }\r\n\r\n    @Test(expectedExceptions = AmazonServiceException.class)\r\n    public void shouldEncapsulateRuntimeFaultException() throws RemoteException {\r\n        VSphereServiceConnectionWithMockedInventoryNavigator service =\r\n                        new VSphereServiceConnectionWithMockedInventoryNavigator();\r\n        InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock();\r\n        when(inventoryNavigatorMock.searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME)).thenThrow(new RuntimeFault());\r\n\r\n        service.describeVirtualMachines();\r\n    }\r\n\r\n    @Test(expectedExceptions = AmazonServiceException.class)\r\n    public void shouldEncapsulateRemoteExceptionException() throws RemoteException {\r\n        VSphereServiceConnectionWithMockedInventoryNavigator service =\r\n                        new VSphereServiceConnectionWithMockedInventoryNavigator();\r\n        InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock();\r\n        when(inventoryNavigatorMock.searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME)).thenThrow(new RemoteException());\r\n\r\n        service.describeVirtualMachines();\r\n    }\r\n\r\n    // The API class ServerConnection is final and can therefore not be mocked.\r\n    // It's possible to work around this using a wrapper, but this is a lot of\r\n    // fake code that needs to be written and tested again just to test that\r\n    // this code really calls the interface method. This is something that rather\r\n    // should be tested in a system test.\r\n\r\n    //@Test\r\n    //    public void shouldDisconnectSeviceByLogoutOverConnection() {\r\n    //        VSphereServiceConnectionWithMockedConnection connection =\r\n    //            new VSphereServiceConnectionWithMockedConnection();\r\n    //\r\n    //        ServiceInstance serviceMock = connection.getService();\r\n    //        ServerConnection serverConnectionMock = mock(ServerConnection.class);\r\n    //        when(serviceMock.getServerConnection()).thenReturn(serverConnectionMock);\r\n    //\r\n    //        connection.disconnect();\r\n    //\r\n    //        verify(serviceMock).getServerConnection();\r\n    //        verify(serverConnectionMock).logout();\r\n    //        assertNull(connection.getService());\r\n    //    }\r\n}\r\n//class VSphereServiceConnectionWithMockedConnection extends VSphereServiceConnection {\r\n//    public VSphereServiceConnectionWithMockedConnection() {\r\n//        super(mock(BasicConfiguration.class));\r\n//        this.setService(mock(ServiceInstance.class));\r\n//    }\r\n//}\r\n\r\nclass VSphereServiceConnectionWithMockedInventoryNavigator extends VSphereServiceConnection {\r\n    private InventoryNavigator inventoryNavigatorMock = mock(InventoryNavigator.class);\r\n\r\n    public VSphereServiceConnectionWithMockedInventoryNavigator() {\r\n        super(mock(BasicConfiguration.class));\r\n    }\r\n\r\n    @Override\r\n    protected InventoryNavigator getInventoryNavigator() {\r\n        return inventoryNavigatorMock;\r\n    }\r\n\r\n    public InventoryNavigator getInventoryNavigatorMock() {\r\n        return inventoryNavigatorMock;\r\n    }\r\n}"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/conformity/TestCrossZoneLoadBalancing.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.conformity;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\n\nimport junit.framework.Assert;\n\nimport org.apache.commons.lang.StringUtils;\nimport org.testng.annotations.BeforeClass;\nimport org.testng.annotations.Test;\n\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.aws.conformity.rule.CrossZoneLoadBalancing;\n\npublic class TestCrossZoneLoadBalancing extends CrossZoneLoadBalancing {\n    private final Map<String, String> asgToElbs = Maps.newHashMap();\n    private final Map<String, Boolean> elbsToCZLB = Maps.newHashMap();\n  \n    @BeforeClass\n    private void init() {\n        asgToElbs.put(\"asg1\", \"elb1,elb2\");\n        asgToElbs.put(\"asg2\", \"elb1\");\n        asgToElbs.put(\"asg3\", \"\");\n        elbsToCZLB.put(\"elb1\", true);\n    }\n\n    @Test\n    public void testDisabledCrossZoneLoadBalancing() {\n        Cluster cluster = new Cluster(\"cluster1\", \"us-east-1\", new AutoScalingGroup(\"asg1\"));\n        Conformity result = check(cluster);\n        Assert.assertEquals(result.getRuleId(), getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 1);\n        Assert.assertEquals(result.getFailedComponents().iterator().next(), \"elb2\");\n    }\n\n    @Test\n    public void testEnabledCrossZoneLoadBalancing() {\n        Cluster cluster = new Cluster(\"cluster1\", \"us-east-1\", new AutoScalingGroup(\"asg2\"));\n        Conformity result = check(cluster);\n        Assert.assertEquals(result.getRuleId(), getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 0);\n    }\n    \n    @Test\n    public void testAsgWithoutElb() {\n        Cluster cluster = new Cluster(\"cluster3\", \"us-east-1\", new AutoScalingGroup(\"asg3\"));\n        Conformity result = check(cluster);\n        Assert.assertEquals(result.getRuleId(), getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 0);\n    }\n    \n    @Override\n    protected List<String> getLoadBalancerNamesForAsg(String region, String asgName) {\n        return Arrays.asList(StringUtils.split(asgToElbs.get(asgName), \",\"));\n    }\n\n    @Override\n    protected boolean isCrossZoneLoadBalancingEnabled(String region, String lbName) {\n        Boolean enabled = elbsToCZLB.get(lbName);\n        return (enabled == null) ? false : enabled;\n    }\n    \n    \n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/conformity/TestSameZonesInElbAndAsg.java",
    "content": "/*\n *\n *  Copyright 2013 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\npackage com.netflix.simianarmy.conformity;\n\nimport com.google.common.collect.Maps;\nimport com.netflix.simianarmy.aws.conformity.rule.SameZonesInElbAndAsg;\nimport junit.framework.Assert;\nimport org.apache.commons.lang.StringUtils;\nimport org.testng.annotations.BeforeClass;\nimport org.testng.annotations.Test;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\n\npublic class TestSameZonesInElbAndAsg extends SameZonesInElbAndAsg {\n    private final Map<String, String> asgToElbs = Maps.newHashMap();\n    private final Map<String, String> asgToZones = Maps.newHashMap();\n    private final Map<String, String> elbToZones = Maps.newHashMap();\n\n    @BeforeClass\n    private void init() {\n        asgToElbs.put(\"asg1\", \"elb1,elb2\");\n        asgToElbs.put(\"asg2\", \"elb2\");\n        asgToElbs.put(\"asg3\", \"\");\n        asgToZones.put(\"asg1\", \"us-east-1a,us-east-1b\");\n        asgToZones.put(\"asg2\", \"us-east-1a\");\n        asgToZones.put(\"asg3\", \"us-east-1b\");\n        elbToZones.put(\"elb1\", \"us-east-1a,us-east-1b\");\n        elbToZones.put(\"elb2\", \"us-east-1a\");\n    }\n\n    @Test\n    public void testZoneMismatch() {\n        Cluster cluster = new Cluster(\"cluster1\", \"us-east-1\", new AutoScalingGroup(\"asg1\"));\n        Conformity result = check(cluster);\n        Assert.assertEquals(result.getRuleId(), getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 1);\n        Assert.assertEquals(result.getFailedComponents().iterator().next(), \"elb2\");\n    }\n\n    @Test\n    public void testZoneMatch() {\n        Cluster cluster = new Cluster(\"cluster2\", \"us-east-1\", new AutoScalingGroup(\"asg2\"));\n        Conformity result = check(cluster);\n        Assert.assertEquals(result.getRuleId(), getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 0);\n    }\n\n    @Test\n    public void testAsgWithoutElb() {\n        Cluster cluster = new Cluster(\"cluster3\", \"us-east-1\", new AutoScalingGroup(\"asg3\"));\n        Conformity result = check(cluster);\n        Assert.assertEquals(result.getRuleId(), getName());\n        Assert.assertEquals(result.getFailedComponents().size(), 0);\n    }\n\n    @Override\n    protected List<String> getLoadBalancerNamesForAsg(String region, String asgName) {\n        return Arrays.asList(StringUtils.split(asgToElbs.get(asgName), \",\"));\n    }\n\n    @Override\n    protected List<String> getAvailabilityZonesForAsg(String region, String asgName) {\n        return Arrays.asList(StringUtils.split(asgToZones.get(asgName), \",\"));\n    }\n\n    @Override\n    protected List<String> getAvailabilityZonesForLoadBalancer(String region, String lbName) {\n        return Arrays.asList(StringUtils.split(elbToZones.get(lbName), \",\"));\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/janitor/TestAbstractJanitor.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n// CHECKSTYLE IGNORE MagicNumberCheck\n\npackage com.netflix.simianarmy.janitor;\n\nimport com.netflix.simianarmy.*;\nimport com.netflix.simianarmy.Resource.CleanupState;\nimport com.netflix.simianarmy.aws.AWSResource;\nimport com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar;\nimport com.netflix.simianarmy.basic.BasicConfiguration;\nimport com.netflix.simianarmy.basic.janitor.BasicJanitorRuleEngine;\nimport org.joda.time.DateTime;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport java.util.*;\n\n\npublic class TestAbstractJanitor extends AbstractJanitor {\n\n    private static final String TEST_REGION = \"test-region\";\n\n    public TestAbstractJanitor(AbstractJanitor.Context ctx, ResourceType resourceType) {\n        super(ctx, resourceType);\n        this.idToResource = new HashMap<>();\n        for (Resource r : ((TestJanitorCrawler) (ctx.janitorCrawler())).getCrawledResources()) {\n            this.idToResource.put(r.getId(), r);\n        }\n    }\n\n    // The collection of all resources for testing.\n    private final Map<String, Resource> idToResource;\n\n    private final HashSet<String> markedResourceIds = new HashSet<>();\n    private final HashSet<String> cleanedResourceIds = new HashSet<>();\n\n    @Override\n    protected void postMark(Resource resource) {\n        markedResourceIds.add(resource.getId());\n    }\n\n    @Override\n    protected void cleanup(Resource resource) {\n        if (!idToResource.containsKey(resource.getId())) {\n            throw new RuntimeException();\n        }\n        // add a special case to throw exception\n        if (resource.getId().equals(\"11\")) {\n            throw new RuntimeException(\"Magic number of id.\");\n        }\n        idToResource.remove(resource.getId());\n    }\n\n    @Override\n    public void cleanupDryRun(Resource resource) throws DryRunnableJanitorException {\n        // simulates a dryRun\n        try {\n            if (!idToResource.containsKey(resource.getId())) {\n                throw new RuntimeException();\n            }\n\n            if (resource.getId().equals(\"11\")) {\n                throw new RuntimeException(\"Magic number of id.\");\n            }\n        } catch (Exception e) {\n            throw new DryRunnableJanitorException(\"Exception during dry run\", e);\n        }\n    }\n\n    @Override\n    protected void postCleanup(Resource resource) {\n        cleanedResourceIds.add(resource.getId());\n    }\n\n    private static List<Resource> generateTestingResources(int n) {\n        List<Resource> resources = new ArrayList<Resource>(n);\n        for (int i = 1; i <= n; i++) {\n            resources.add(new AWSResource().withId(String.valueOf(i))\n                    .withRegion(TEST_REGION)\n                    .withResourceType(TestResourceType.TEST_RESOURCE_TYPE)\n                    .withOptOutOfJanitor(false));\n        }\n        return resources;\n    }\n\n    @Test\n    public static void testJanitor() {\n        int n = 10;\n        Collection<Resource> crawledResources = new ArrayList<>(generateTestingResources(n));\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(new HashMap<>());\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new IsEvenRule()),\n                        crawler,\n                        resourceTracker,\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(false);\n\n        Assert.assertEquals(crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(), n);\n        Assert.assertEquals(janitor.markedResourceIds.size(), 0);\n\n        janitor.markResources();\n\n        Assert.assertEquals(janitor.getMarkedResources().size(), n / 2);\n        Assert.assertEquals(janitor.markedResourceIds.size(), n / 2);\n        for (int i = 1; i <= n; i += 2) {\n            Assert.assertTrue(janitor.markedResourceIds.contains(String.valueOf(i)));\n        }\n\n        Assert.assertEquals(janitor.cleanedResourceIds.size(), 0);\n        janitor.cleanupResources();\n\n        Assert.assertEquals(janitor.getCleanedResources().size(), n / 2);\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.JANITOR_TERMINATED, TEST_REGION).size(),\n                n / 2);\n        Assert.assertEquals(janitor.cleanedResourceIds.size(), n / 2);\n        for (int i = 1; i <= n; i += 2) {\n            Assert.assertTrue(janitor.cleanedResourceIds.contains(String.valueOf(i)));\n        }\n\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size());\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0);\n    }\n\n    @Test\n    public static void testJanitorWithOptedOutResources() {\n        Collection<Resource> crawledResources = new ArrayList<Resource>();\n        int n = 10;\n        for (Resource r : generateTestingResources(n)) {\n            crawledResources.add(r);\n        }\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        // set some resources in the tracker as opted out\n        Date now = new Date(DateTime.now().minusDays(1).getMillis());\n        Map<String, Resource> trackedResources = new HashMap<String, Resource>();\n        for (Resource r : generateTestingResources(n)) {\n            int id = Integer.parseInt(r.getId());\n            if (id % 4 == 1 || id % 4 == 2) {\n                r.setOptOutOfJanitor(true);\n                r.setState(CleanupState.MARKED);\n                r.setExpectedTerminationTime(now);\n                r.setMarkTime(now);\n            }\n            trackedResources.put(r.getId(), r);\n        }\n        TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(\n                trackedResources);\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new IsEvenRule()),\n                        crawler,\n                        resourceTracker,\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(false);\n        Assert.assertEquals(\n                crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(),\n                10);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(),\n                6); // 1, 2, 5, 6, 9, 10 are marked\n        Assert.assertEquals(janitor.markedResourceIds.size(), 0);\n        janitor.markResources();\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(),\n                5); // 1, 3, 5, 7, 9 are marked\n        Assert.assertEquals(janitor.getMarkedResources().size(), 2); // 3, 7 are newly marked.\n        Assert.assertEquals(janitor.markedResourceIds.size(), 2);\n        Assert.assertEquals(janitor.cleanedResourceIds.size(), 0);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(),\n                5); // 1, 3, 5, 7, 9 are marked\n        Assert.assertEquals(janitor.getUnmarkedResources().size(), 3); // 2, 6, 10 got unmarked\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.UNMARKED, TEST_REGION).size(),\n                3);\n        janitor.cleanupResources();\n        Assert.assertEquals(janitor.getCleanedResources().size(), 2); // 3, 7 are cleaned\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.JANITOR_TERMINATED, TEST_REGION).size(),\n                2);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size());\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0);\n        Assert.assertEquals(janitor.getUnmarkedResourcesCount(), 3);\n    }\n\n    @Test\n    public static void testJanitorWithCleanupFailure() {\n        Collection<Resource> crawledResources = new ArrayList<Resource>();\n        int n = 20;\n        for (Resource r : generateTestingResources(n)) {\n            crawledResources.add(r);\n        }\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new IsEvenRule()),\n                        crawler,\n                        new TestJanitorResourceTracker(new HashMap<String, Resource>()),\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(false);\n        Assert.assertEquals(\n                crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(),\n                n);\n        janitor.markResources();\n        Assert.assertEquals(janitor.getMarkedResources().size(), n / 2);\n\n        janitor.cleanupResources();\n        Assert.assertEquals(janitor.getCleanedResources().size(), n / 2 - 1);\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 1);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size());\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 1);\n    }\n\n    private static TestAbstractJanitor getJanitor(int numberOfCrawledResources, boolean leashed) {\n        TestJanitorCrawler crawler = new TestJanitorCrawler(generateTestingResources(numberOfCrawledResources));\n        JanitorRuleEngine rulesEngine = new BasicJanitorRuleEngine().addRule(new IsEvenRule());\n        JanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(new HashMap<>());\n        TestJanitorContext janitorContext = new TestJanitorContext(TEST_REGION, rulesEngine, crawler, resourceTracker, new TestMonkeyCalendar());\n        TestAbstractJanitor janitor = new TestAbstractJanitor(janitorContext, TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(leashed);\n        return janitor;\n    }\n\n    @Test\n    public static void testCleanupDryRunOnWithJanitorOnLeashWithAFailure() {\n        int n = 20;\n        TestAbstractJanitor janitor = getJanitor(n, true);\n        janitor.markResources();\n        Assert.assertEquals(janitor.getMarkedResources().size(), n / 2);\n\n        janitor.cleanupResources();\n\n        Assert.assertEquals(janitor.getCleanedResources().size(), 0);\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), 0);\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0);\n        Assert.assertEquals(janitor.getCleanupDryRunFailureCount().getValue().intValue(), 1);\n    }\n\n    @Test\n    public static void testJanitorWithUnmarking() {\n        Collection<Resource> crawledResources = new ArrayList<Resource>();\n        Map<String, Resource> trackedResources = new HashMap<String, Resource>();\n        int n = 10;\n        DateTime now = DateTime.now();\n        Date markTime = new Date(now.minusDays(5).getMillis());\n        Date notifyTime = new Date(now.minusDays(4).getMillis());\n        Date terminationTime = new Date(now.minusDays(1).getMillis());\n        for (Resource r : generateTestingResources(n)) {\n            if (Integer.parseInt(r.getId()) % 3 == 0) {\n                trackedResources.put(r.getId(), r);\n                r.setState(CleanupState.MARKED);\n                r.setMarkTime(markTime);\n                r.setExpectedTerminationTime(terminationTime);\n                r.setNotificationTime(notifyTime);\n            }\n        }\n        for (Resource r : generateTestingResources(n)) {\n            crawledResources.add(r);\n        }\n\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(trackedResources);\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new IsEvenRule()),\n                        crawler,\n                        resourceTracker,\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(false);\n        Assert.assertEquals(\n                crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(),\n                n);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(),\n                n / 3);\n        janitor.markResources();\n        // (n/3-n/6) resources were already marked, so in the last run the marked resources\n        // should be n/2 - n/3 + n/6.\n        Assert.assertEquals(janitor.getMarkedResources().size(), n / 2 - n / 3 + n / 6);\n        Assert.assertEquals(janitor.getUnmarkedResources().size(), n / 6);\n\n        janitor.cleanupResources();\n        Assert.assertEquals(janitor.getCleanedResources().size(), n / 2);\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size());\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0);\n        Assert.assertEquals(janitor.getUnmarkedResourcesCount(), n/6);\n    }\n\n\n    @Test\n    public static void testJanitorWithFutureTerminationTime() {\n        Collection<Resource> crawledResources = new ArrayList<Resource>();\n        Map<String, Resource> trackedResources = new HashMap<String, Resource>();\n        int n = 10;\n        DateTime now = DateTime.now();\n        Date markTime = new Date(now.minusDays(5).getMillis());\n        Date notifyTime = new Date(now.minusDays(4).getMillis());\n        Date terminationTime = new Date(now.plusDays(10).getMillis());\n        for (Resource r : generateTestingResources(n)) {\n            trackedResources.put(r.getId(), r);\n            r.setState(CleanupState.MARKED);\n            r.setNotificationTime(notifyTime);\n            r.setMarkTime(markTime);\n            r.setExpectedTerminationTime(terminationTime);\n        }\n        for (Resource r : generateTestingResources(n)) {\n            crawledResources.add(r);\n        }\n\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(trackedResources);\n\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new IsEvenRule()),\n                        crawler,\n                        resourceTracker,\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(false);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(),\n                n);\n        janitor.cleanupResources();\n        Assert.assertEquals(janitor.getCleanedResources().size(), 0);\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size());\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0);\n    }\n\n\n    @Test\n    public static void testJanitorWithoutNotification() {\n        Collection<Resource> crawledResources = new ArrayList<Resource>();\n        Map<String, Resource> trackedResources = new HashMap<String, Resource>();\n        int n = 10;\n        for (Resource r : generateTestingResources(n)) {\n            trackedResources.put(r.getId(), r);\n            r.setState(CleanupState.MARKED);\n            // The marking/cleanup is not notified so we the Janitor won't clean it up.\n            // r.setNotificationTime(new Date());\n            r.setMarkTime(new Date());\n            r.setExpectedTerminationTime(new Date(DateTime.now().plusDays(10).getMillis()));\n        }\n        for (Resource r : generateTestingResources(n)) {\n            crawledResources.add(r);\n        }\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(trackedResources);\n\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new IsEvenRule()),\n                        crawler,\n                        resourceTracker,\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(false);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(),\n                n);\n\n        janitor.cleanupResources();\n        Assert.assertEquals(janitor.getCleanedResources().size(), 0);\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size());\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0);\n    }\n\n    @Test\n    public static void testLeashedJanitorForMarking() {\n        Collection<Resource> crawledResources = new ArrayList<Resource>();\n        int n = 10;\n        for (Resource r : generateTestingResources(n)) {\n            crawledResources.add(r);\n        }\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(\n                new HashMap<String, Resource>());\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new IsEvenRule()),\n                        crawler,\n                        resourceTracker,\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(true);\n        Assert.assertEquals(\n                crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(),\n                n);\n        janitor.markResources();\n        Assert.assertEquals(janitor.getMarkedResources().size(), n / 2);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), n / 2);\n    }\n\n\n    @Test\n    public static void testJanitorWithoutHoldingOffCleanup() {\n        Collection<Resource> crawledResources = new ArrayList<Resource>();\n        int n = 10;\n        for (Resource r : generateTestingResources(n)) {\n            crawledResources.add(r);\n        }\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(new HashMap<String, Resource>());\n        DateTime now = DateTime.now();\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new ImmediateCleanupRule(now)),\n                        crawler,\n                        resourceTracker,\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(false);\n        Assert.assertEquals(\n                crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(),\n                n);\n        Assert.assertEquals(janitor.markedResourceIds.size(), 0);\n        janitor.markResources();\n        Assert.assertEquals(janitor.getMarkedResources().size(), n);\n        Assert.assertEquals(janitor.markedResourceIds.size(), n);\n        for (int i = 1; i <= n; i++) {\n            Assert.assertTrue(janitor.markedResourceIds.contains(String.valueOf(i)));\n        }\n\n        Assert.assertEquals(janitor.cleanedResourceIds.size(), 0);\n        janitor.cleanupResources();\n        // No resource is cleaned since the notification is later than expected termination time.\n        Assert.assertEquals(janitor.getCleanedResources().size(), n);\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.JANITOR_TERMINATED, TEST_REGION).size(),\n                n);\n        Assert.assertEquals(janitor.cleanedResourceIds.size(), n);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size());\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0);\n    }\n\n//    @Test TODO: disable while debugging issues with this functionality\n    public static void testJanitorWithUnmarkingUserTerminated() {\n        Collection<Resource> crawledResources = new ArrayList<Resource>();\n        Map<String, Resource> trackedResources = new HashMap<String, Resource>();\n        int n = 10;\n        DateTime now = DateTime.now();\n        Date markTime = new Date(now.minusDays(5).getMillis());\n        Date notifyTime = new Date(now.minusDays(4).getMillis());\n        Date terminationTime = new Date(now.minusDays(1).getMillis());\n        for (Resource r : generateTestingResources(n)) {\n            if (Integer.parseInt(r.getId()) % 3 != 0) {\n                crawledResources.add(r);\n            } else {\n                trackedResources.put(r.getId(), r);\n                r.setState(CleanupState.MARKED);\n                r.setMarkTime(markTime);\n                r.setNotificationTime(notifyTime);\n                r.setExpectedTerminationTime(terminationTime);\n            }\n        }\n\n        TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources);\n        TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(trackedResources);\n        TestAbstractJanitor janitor = new TestAbstractJanitor(\n                new TestJanitorContext(TEST_REGION,\n                        new BasicJanitorRuleEngine().addRule(new IsEvenRule()),\n                        crawler,\n                        resourceTracker,\n                        new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE);\n        janitor.setLeashed(false);\n        Assert.assertEquals(\n                crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(),\n                n - n / 3);\n        Assert.assertEquals(resourceTracker.getResources(\n                TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(),\n                n / 3);\n        janitor.markResources();\n        // n/3 resources should be considered user terminated\n        Assert.assertEquals(janitor.getMarkedResources().size(), n / 2 - n / 3 + n / 6);\n        Assert.assertEquals(janitor.getUnmarkedResources().size(), n / 3);\n\n        janitor.cleanupResources();\n        Assert.assertEquals(janitor.getCleanedResources().size(), n / 2 - n / 3 + n / 6);\n        Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0);\n        Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size());\n        Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size());\n        Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0);\n        Assert.assertEquals(janitor.getUnmarkedResourcesCount(), n / 3);\n    }\n}\n\nclass TestJanitorCrawler implements JanitorCrawler {\n    private final Collection<Resource> crawledResources;\n    public Collection<Resource> getCrawledResources() {\n        return crawledResources;\n    }\n\n    public TestJanitorCrawler(Collection<Resource> crawledResources) {\n        this.crawledResources = crawledResources;\n    }\n\n    @Override\n    public EnumSet<? extends ResourceType> resourceTypes() {\n        return EnumSet.of(TestResourceType.TEST_RESOURCE_TYPE);\n    }\n\n    @Override\n    public List<Resource> resources(ResourceType resourceType) {\n        return new ArrayList<Resource>(crawledResources);\n    }\n\n    @Override\n    public List<Resource> resources(String... resourceIds) {\n        List<Resource> result = new ArrayList<Resource>(resourceIds.length);\n        Set<String> idSet = new HashSet<String>(Arrays.asList(resourceIds));\n        for (Resource r : crawledResources) {\n            if (idSet.contains(r.getId())) {\n                result.add(r);\n            }\n        }\n        return result;\n    }\n\n    @Override\n    public String getOwnerEmailForResource(Resource resource) {\n        return null;\n    }\n}\n\nenum TestResourceType implements ResourceType {\n    TEST_RESOURCE_TYPE\n}\n\nclass TestJanitorResourceTracker implements JanitorResourceTracker {\n    private final Map<String, Resource> resources;\n    public TestJanitorResourceTracker(Map<String, Resource> trackedResources) {\n        this.resources = trackedResources;\n    }\n\n    @Override\n    public void addOrUpdate(Resource resource) {\n        resources.put(resource.getId(), resource);\n    }\n\n    @Override\n    public List<Resource> getResources(ResourceType resourceType, CleanupState state, String region) {\n        List<Resource> result = new ArrayList<Resource>();\n        for (Resource r : resources.values()) {\n            if (r.getResourceType().equals(resourceType)\n                    && (r.getState() != null && r.getState().equals(state))\n                    && r.getRegion().equals(region)) {\n                result.add(r.cloneResource());\n            }\n        }\n        return result;\n    }\n\n    @Override\n    public Resource getResource(String resourceId) {\n        return resources.get(resourceId);\n    }\n\n    @Override\n    public Resource getResource(String resourceId, String region) {\n        return resources.get(resourceId);\n    }\n\n}\n\n/**\n * The rule considers all resources with an odd number as the id as cleanup candidate.\n */\nclass IsEvenRule implements Rule {\n    @Override\n    public boolean isValid(Resource resource) {\n        // returns true if the resource's id is an even integer\n        int id;\n        try {\n            id = Integer.parseInt(resource.getId());\n        } catch (Exception e) {\n            return true;\n        }\n        DateTime now = DateTime.now();\n        resource.setExpectedTerminationTime(new Date(now.minusDays(1).getMillis()));\n        // Set the resource as notified so it can be cleaned\n        // set the notification time at more than 1 day before the termination time\n        resource.setNotificationTime(new Date(now.minusDays(4).getMillis()));\n        return id % 2 == 0;\n    }\n}\n\n/**\n * The rule considers all resources as cleanup candidate and sets notification time\n * after the termination time.\n */\nclass ImmediateCleanupRule implements Rule {\n    private final DateTime now;\n    public ImmediateCleanupRule(DateTime now) {\n        this.now = now;\n    }\n    @Override\n    public boolean isValid(Resource resource) {\n        resource.setExpectedTerminationTime(new Date(now.minusMinutes(10).getMillis()));\n        resource.setNotificationTime(new Date(now.getMillis()-5000));\n        return false;\n    }\n}\n\nclass TestJanitorContext implements AbstractJanitor.Context {\n    private final String region;\n    private final JanitorRuleEngine ruleEngine;\n    private final JanitorCrawler crawler;\n    private final JanitorResourceTracker resourceTracker;\n    private final MonkeyCalendar calendar;\n\n    public TestJanitorContext(String region, JanitorRuleEngine ruleEngine, JanitorCrawler crawler,\n            JanitorResourceTracker resourceTracker, MonkeyCalendar calendar) {\n        this.region = region;\n        this.resourceTracker = resourceTracker;\n        this.ruleEngine = ruleEngine;\n        this.crawler = crawler;\n        this.calendar = calendar;\n    }\n\n    @Override\n    public String region() {\n        return region;\n    }\n\n    @Override\n    public MonkeyCalendar calendar() {\n        return calendar;\n    }\n\n    @Override\n    public JanitorRuleEngine janitorRuleEngine() {\n        return ruleEngine;\n    }\n\n    @Override\n    public JanitorCrawler janitorCrawler() {\n        return crawler;\n    }\n\n    @Override\n    public JanitorResourceTracker janitorResourceTracker() {\n        return resourceTracker;\n    }\n\n    @Override\n    public MonkeyConfiguration configuration() {\n        return new BasicConfiguration(new Properties());\n    }\n\n    @Override\n    public MonkeyRecorder recorder() {\n        // No events to be recorded\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/janitor/TestBasicJanitorMonkeyContext.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.janitor;\n\nimport com.netflix.simianarmy.aws.janitor.rule.generic.UntaggedRule;\nimport com.netflix.simianarmy.basic.TestBasicCalendar;\nimport com.netflix.simianarmy.basic.janitor.BasicJanitorRuleEngine;\nimport org.apache.commons.lang.StringUtils;\nimport org.testng.Assert;\nimport org.testng.annotations.BeforeMethod;\nimport org.testng.annotations.Test;\n\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * The basic implementation of the context class for Janitor monkey.\n */\npublic class TestBasicJanitorMonkeyContext {\n    \n    private static final int SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOWNER = 3;\n    \n    private static final int SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOUTOWNER = 8;\n    \n    private static final Boolean SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_ENABLED = true;\n    \n    private static final Set<String> SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_REQUIREDTAGS = new HashSet<String>(Arrays.asList(\"owner\", \"costcenter\"));\n    \n    private static final String SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RESOURCES = \"Instance\";\n    \n    private String monkeyRegion;\n\n    private TestBasicCalendar monkeyCalendar;\n\n    public TestBasicJanitorMonkeyContext() {\n        super();\n    }\n    \n    @BeforeMethod\n    public void before() {\n        monkeyRegion = \"us-east-1\";\n        monkeyCalendar = new TestBasicCalendar();\n    }\n    \n    @Test\n    public void testAddRuleWithUntaggedRuleResource() {\n        JanitorRuleEngine ruleEngine = new BasicJanitorRuleEngine();\n        Boolean untaggedRuleEnabled = new Boolean(true);\n\n        Rule rule = new UntaggedRule(monkeyCalendar, SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_REQUIREDTAGS,\n                SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOWNER,\n                        SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOUTOWNER);\n        if (untaggedRuleEnabled && getUntaggedRuleResourceSet().contains(\"INSTANCE\")) {\n            ruleEngine.addRule(rule);\n        }\n        Assert.assertTrue(ruleEngine.getRules().contains(rule));\n    }\n\n    @Test\n    public void testAddRuleWithoutUntaggedRuleResource() {\n        JanitorRuleEngine ruleEngine = new BasicJanitorRuleEngine();\n        Boolean untaggedRuleEnabled = new Boolean(true);\n\n        Rule rule = new UntaggedRule(monkeyCalendar, SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_REQUIREDTAGS,\n                SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOWNER,\n                        SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOUTOWNER);\n        if (untaggedRuleEnabled && getUntaggedRuleResourceSet().contains(\"ASG\")) {\n            ruleEngine.addRule(rule);\n        }\n        Assert.assertFalse(ruleEngine.getRules().contains(rule));\n    }\n\n    private Set<String> getUntaggedRuleResourceSet() {\n        Set<String> untaggedRuleResourceSet = new HashSet<String>();\n        if (SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_ENABLED) {\n            String untaggedRuleResources = SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RESOURCES;\n            if (StringUtils.isNotBlank(untaggedRuleResources)) {\n                for (String resourceType : untaggedRuleResources.split(\",\")) {\n                    untaggedRuleResourceSet.add(resourceType.trim().toUpperCase());\n                }\n            }\n        }\n        return untaggedRuleResourceSet;\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/resources/chaos/TestChaosMonkeyResource.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\n// CHECKSTYLE IGNORE Javadoc\n//CHECKSTYLE IGNORE MagicNumber\npackage com.netflix.simianarmy.resources.chaos;\n\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Matchers.anyMap;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nimport java.util.Arrays;\nimport java.util.Date;\nimport java.util.Map;\nimport java.util.Scanner;\n\nimport javax.ws.rs.core.MultivaluedMap;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.UriInfo;\n\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Captor;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.testng.Assert;\nimport org.testng.annotations.BeforeTest;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.EventType;\nimport com.netflix.simianarmy.MonkeyRecorder;\nimport com.netflix.simianarmy.MonkeyRunner;\nimport com.netflix.simianarmy.MonkeyType;\nimport com.netflix.simianarmy.basic.BasicRecorderEvent;\nimport com.netflix.simianarmy.basic.chaos.BasicChaosMonkey;\nimport com.netflix.simianarmy.chaos.ChaosMonkey;\nimport com.netflix.simianarmy.chaos.TestChaosMonkeyContext;\nimport com.sun.jersey.core.util.MultivaluedMapImpl;\n\npublic class TestChaosMonkeyResource {\n    private static final Logger LOGGER = LoggerFactory.getLogger(TestChaosMonkeyResource.class);\n\n    @Captor\n    private ArgumentCaptor<MonkeyType> monkeyTypeArg;\n    @Captor\n    private ArgumentCaptor<EventType> eventTypeArg;\n    @Captor\n    private ArgumentCaptor<Map<String, String>> queryArg;\n    @Captor\n    private ArgumentCaptor<Date> dateArg;\n\n    @Mock\n    private UriInfo mockUriInfo;\n    @Mock\n    private static MonkeyRecorder mockRecorder;\n\n    @BeforeTest\n    public void init() {\n        MockitoAnnotations.initMocks(this);\n    }\n\n    @Test\n    void testTerminateNow() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"ondemandTermination.properties\");\n\n        String input = \"{\\\"eventType\\\":\\\"CHAOS_TERMINATION\\\",\\\"groupType\\\":\\\"TYPE_C\\\",\\\"groupName\\\":\\\"name4\\\"}\";\n\n        Assert.assertEquals(ctx.selectedOn().size(), 0);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n\n        ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.OK);\n        Assert.assertEquals(ctx.selectedOn().size(), 1);\n        Assert.assertEquals(ctx.terminated().size(), 1);\n\n        validateAddEventResult(resource, input, Response.Status.OK);\n        Assert.assertEquals(ctx.selectedOn().size(), 2);\n        Assert.assertEquals(ctx.terminated().size(), 2);\n\n        // TYPE_C.name4 only has two instances, so the 3rd ondemand termination\n        // will not terminate anything.\n        validateAddEventResult(resource, input, Response.Status.GONE);\n        Assert.assertEquals(ctx.selectedOn().size(), 3);\n        Assert.assertEquals(ctx.terminated().size(), 2);\n\n        // Try a different type will work\n        input = \"{\\\"eventType\\\":\\\"CHAOS_TERMINATION\\\",\\\"groupType\\\":\\\"TYPE_C\\\",\\\"groupName\\\":\\\"name5\\\"}\";\n        validateAddEventResult(resource, input, Response.Status.OK);\n        Assert.assertEquals(ctx.selectedOn().size(), 4);\n        Assert.assertEquals(ctx.terminated().size(), 3);\n    }\n\n    @Test\n    void testTerminateNowDisabled() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"ondemandTerminationDisabled.properties\");\n        String input = \"{\\\"eventType\\\":\\\"CHAOS_TERMINATION\\\",\\\"groupType\\\":\\\"TYPE_C\\\",\\\"groupName\\\":\\\"name4\\\"}\";\n\n        Assert.assertEquals(ctx.selectedOn().size(), 0);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n\n        ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.FORBIDDEN);\n        Assert.assertEquals(ctx.selectedOn().size(), 0);\n        Assert.assertEquals(ctx.terminated().size(), 0);\n    }\n\n    @Test\n    void testTerminateNowBadInput() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"ondemandTermination.properties\");\n        String input = \"{\\\"groupType\\\":\\\"TYPE_C\\\",\\\"groupName\\\":\\\"name4\\\"}\";\n\n        ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.BAD_REQUEST);\n\n        input = \"{\\\"eventType\\\":\\\"CHAOS_TERMINATION\\\",\\\"groupName\\\":\\\"name4\\\"}\";\n        resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.BAD_REQUEST);\n\n        input = \"{\\\"eventType\\\":\\\"CHAOS_TERMINATION\\\",\\\"groupType\\\":\\\"TYPE_C\\\"}\";\n        resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.BAD_REQUEST);\n    }\n\n    @Test\n    void testTerminateNowBadGroupNotExist() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"ondemandTermination.properties\");\n\n        String input = \"{\\\"eventType\\\":\\\"CHAOS_TERMINATION\\\",\\\"groupType\\\":\\\"INVALID\\\",\\\"groupName\\\":\\\"name4\\\"}\";\n        ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.NOT_FOUND);\n\n        input = \"{\\\"eventType\\\":\\\"CHAOS_TERMINATION\\\",\\\"groupType\\\":\\\"TYPE_C\\\",\\\"groupName\\\":\\\"INVALID\\\"}\";\n        resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.NOT_FOUND);\n    }\n\n    @Test\n    void testTerminateNowBadEventType() {\n        TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"ondemandTermination.properties\");\n\n        String input = \"{\\\"eventType\\\":\\\"INVALID\\\",\\\"groupType\\\":\\\"TYPE_C\\\",\\\"groupName\\\":\\\"name4\\\"}\";\n        ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx));\n        validateAddEventResult(resource, input, Response.Status.BAD_REQUEST);\n    }\n\n    @Test\n    public void testResource() {\n        MonkeyRunner.getInstance().replaceMonkey(BasicChaosMonkey.class, MockTestChaosMonkeyContext.class);\n\n        ChaosMonkeyResource resource = new ChaosMonkeyResource();\n\n        MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();\n        queryParams.add(\"groupType\", \"ASG\");\n        Date queryDate = new Date();\n        queryParams.add(\"since\", String.valueOf(queryDate.getTime()));\n\n        when(mockUriInfo.getQueryParameters()).thenReturn(queryParams);\n\n        @SuppressWarnings(\"unchecked\")\n        // fix when Matcher.anyMapOf is available\n        Map<String, String> anyMap = anyMap();\n\n        when(mockRecorder.findEvents(any(MonkeyType.class), any(EventType.class), anyMap, any(Date.class))).thenReturn(\n                Arrays.asList(mkEvent(\"i-123456789012345670\"), mkEvent(\"i-123456789012345671\")));\n\n        try {\n            Response resp = resource.getChaosEvents(mockUriInfo);\n            Assert.assertEquals(resp.getEntity().toString(), getResource(\"getChaosEventsResponse.json\"));\n        } catch (Exception e) {\n            LOGGER.error(\"exception from getChaosEvents\", e);\n            Assert.fail(\"getChaosEvents throws exception\");\n        }\n\n        verify(mockRecorder).findEvents(monkeyTypeArg.capture(), eventTypeArg.capture(), queryArg.capture(),\n                dateArg.capture());\n\n        Assert.assertEquals(monkeyTypeArg.getValue(), ChaosMonkey.Type.CHAOS);\n        Assert.assertEquals(eventTypeArg.getValue(), ChaosMonkey.EventTypes.CHAOS_TERMINATION);\n        Map<String, String> query = queryArg.getValue();\n        Assert.assertEquals(query.size(), 1);\n        Assert.assertEquals(query.get(\"groupType\"), \"ASG\");\n        Assert.assertEquals(dateArg.getValue(), queryDate);\n    }\n\n    private MonkeyRecorder.Event mkEvent(String instance) {\n        final MonkeyType monkeyType = ChaosMonkey.Type.CHAOS;\n        final EventType eventType = ChaosMonkey.EventTypes.CHAOS_TERMINATION;\n        // SUPPRESS CHECKSTYLE MagicNumber\n        return new BasicRecorderEvent(monkeyType, eventType, \"region\", instance, 1330538400000L)\n        .addField(\"groupType\", \"ASG\").addField(\"groupName\", \"testGroup\");\n    }\n\n    public static class MockTestChaosMonkeyContext extends TestChaosMonkeyContext {\n        @Override\n        public MonkeyRecorder recorder() {\n            return mockRecorder;\n        }\n    }\n\n    String getResource(String name) {\n        // get resource as stream, use Scanner to read stream as one token\n        return new Scanner(TestChaosMonkeyResource.class.getResourceAsStream(name), \"UTF-8\").useDelimiter(\"\\\\A\").next();\n    }\n\n    private void validateAddEventResult(ChaosMonkeyResource resource, String input, Response.Status responseStatus) {\n        try {\n            Response resp = resource.addEvent(input);\n            Assert.assertEquals(resp.getStatus(), responseStatus.getStatusCode());\n        } catch (Exception e) {\n            LOGGER.error(\"exception from addEvent\", e);\n            Assert.fail(\"addEvent throws exception\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/java/com/netflix/simianarmy/tunable/TestTunablyAggressiveChaosMonkey.java",
    "content": "/*\n *\n *  Copyright 2012 Netflix, Inc.\n *\n *     Licensed under the Apache License, Version 2.0 (the \"License\");\n *     you may not use this file except in compliance with the License.\n *     You may obtain a copy of the License at\n *\n *         http://www.apache.org/licenses/LICENSE-2.0\n *\n *     Unless required by applicable law or agreed to in writing, software\n *     distributed under the License is distributed on an \"AS IS\" BASIS,\n *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *     See the License for the specific language governing permissions and\n *     limitations under the License.\n *\n */\npackage com.netflix.simianarmy.tunable;\n\nimport com.amazonaws.services.autoscaling.model.TagDescription;\nimport org.testng.Assert;\nimport org.testng.annotations.Test;\n\nimport com.netflix.simianarmy.GroupType;\nimport com.netflix.simianarmy.basic.chaos.BasicInstanceGroup;\nimport com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup;\nimport com.netflix.simianarmy.chaos.TestChaosMonkeyContext;\n\nimport java.util.Collections;\n\npublic class TestTunablyAggressiveChaosMonkey {\n  private enum GroupTypes implements GroupType {\n    TYPE_A, TYPE_B\n  };\n\n  @Test\n  public void testFullProbability_basic() {\n    TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"fullProbability.properties\");\n\n    TunablyAggressiveChaosMonkey chaos = new TunablyAggressiveChaosMonkey(ctx);\n\n    InstanceGroup basic = new BasicInstanceGroup(\"basic\", GroupTypes.TYPE_A, \"region\", Collections.<TagDescription>emptyList());\n    \n    double probability = chaos.getEffectiveProbability(basic);\n    \n    Assert.assertEquals(probability, 1.0);\n  }\n\n  @Test\n  public void testFullProbability_tuned() {\n    TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(\"fullProbability.properties\");\n\n    TunablyAggressiveChaosMonkey chaos = new TunablyAggressiveChaosMonkey(ctx);\n\n    TunableInstanceGroup tuned = new TunableInstanceGroup(\"basic\", GroupTypes.TYPE_A, \"region\", Collections.<TagDescription>emptyList());\n    tuned.setAggressionCoefficient(0.5);\n    \n    double probability = chaos.getEffectiveProbability(tuned);\n    \n    Assert.assertEquals(probability, 0.5);\n  }\n}\n"
  },
  {
    "path": "src/test/resources/chaos.properties",
    "content": "simianarmy.chaos.enabled = false\n"
  },
  {
    "path": "src/test/resources/client.properties",
    "content": "simianarmy.client.aws.accountKey = fakeAccount\r\nsimianarmy.client.aws.secretKey  = fakeSecret\r\nsimianarmy.client.aws.assumeRoleArn = arn:aws:iam::fakeAccount:role/fakeRole\r\nsimianarmy.client.aws.accountName = default\r\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/all.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true\nsimianarmy.chaos.TYPE_B.enabled = true\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/cloudformation.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_D.new-group-TestGroup1.enabled = true\nsimianarmy.chaos.TYPE_D.new-group-TestGroup1.probability = 1.0\nsimianarmy.chaos.TYPE_D.new-group-TestGroup1.maxTerminationsPerDay = 100\nsimianarmy.chaos.TYPE_D.new-group-TestGroup1.lastOptInTimeInMilliseconds=2000\n\nsimianarmy.chaos.TYPE_D.new-group-TestGroup1.notification.enabled = true\nsimianarmy.chaos.TYPE_D.new-group-TestGroup1.ownerEmail = foo@bar.com\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/disabled.properties",
    "content": "simianarmy.chaos.enabled = false"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/enabledA.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.TYPE_A.enabled = true\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/enabledAwith0.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = false\nsimianarmy.chaos.TYPE_A.name0.enabled = true"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/enabledAwithout1.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true\nsimianarmy.chaos.TYPE_A.name1.enabled = false"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/enabledB.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.TYPE_A.enabled = false\nsimianarmy.chaos.TYPE_B.enabled = true"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/fullProbability.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true\nsimianarmy.chaos.TYPE_A.probability = 1.0\nsimianarmy.chaos.TYPE_B.enabled = true\nsimianarmy.chaos.TYPE_B.probability = 1.0\nsimianarmy.scheduler.frequency = 1\nsimianarmy.scheduler.frequencyUnit = DAYS\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/globalNotificationEnabled.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true\nsimianarmy.chaos.TYPE_B.enabled = true\nsimianarmy.chaos.TYPE_A.name0.notification.enabled = false\nsimianarmy.chaos.TYPE_A.name1.notification.enabled = true\nsimianarmy.chaos.notification.global.enabled = true\nsimianarmy.chaos.notification.global.receiverEmail = test@email.com"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationDisabled.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\n\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 0\n# Set the last opt-in time to be a pretty distant past\nsimianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999\n\nsimianarmy.chaos.mandatoryTermination.enabled = false\nsimianarmy.chaos.mandatoryTermination.windowInDays = 10\nsimianarmy.chaos.mandatoryTermination.defaultProbability = 1.0\n\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationInsideWindow.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\n\nsimianarmy.chaos.terminateOndemand.enabled = true\nsimianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 10\n\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 0\n# Set the last opt-in time to be a pretty distant past\nsimianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999\n\nsimianarmy.chaos.mandatoryTermination.enabled = true\n# The window size is big enough so the opt-in time is inside the window\nsimianarmy.chaos.mandatoryTermination.windowInDays = 100000\nsimianarmy.chaos.mandatoryTermination.defaultProbability = 1.0\n\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationNoOptInTime.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\n\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 0\n\n# No last opt-in time is specified\n#simianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999\n\nsimianarmy.chaos.mandatoryTermination.enabled = true\nsimianarmy.chaos.mandatoryTermination.windowInDays = 10\nsimianarmy.chaos.mandatoryTermination.defaultProbability = 1.0\n\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationNotDefined.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\n\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 0\n# Set the last opt-in time to be a pretty distant past\nsimianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999\n\n# Not having the mandatoryTerminationDisabled.properties is the same\n# as defining it as false\n#simianarmy.chaos.mandatoryTermination.enabled = false\nsimianarmy.chaos.mandatoryTermination.windowInDays = 10\nsimianarmy.chaos.mandatoryTermination.defaultProbability = 1.0\n\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationOutsideWindow.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\n\nsimianarmy.chaos.terminateOndemand.enabled = true\nsimianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 10\n\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 0\n# Set the last opt-in time to be a pretty distant past\nsimianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999\n\nsimianarmy.chaos.mandatoryTermination.enabled = true\n# The window size is small enough so the opt-in time is outside the window\nsimianarmy.chaos.mandatoryTermination.windowInDays = 10\nsimianarmy.chaos.mandatoryTermination.defaultProbability = 1.0\n\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/noProbability.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true\nsimianarmy.chaos.TYPE_A.probability = 0.0\nsimianarmy.chaos.TYPE_B.enabled = true\nsimianarmy.chaos.TYPE_B.probability = 0.0\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/noProbabilityByName.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true\nsimianarmy.chaos.TYPE_A.probability = 1.0\nsimianarmy.chaos.TYPE_A.name0.probability = 0.0\nsimianarmy.chaos.TYPE_A.name1.probability = 0.0\nsimianarmy.chaos.TYPE_B.enabled = true\nsimianarmy.chaos.TYPE_B.probability = 1.0\nsimianarmy.chaos.TYPE_B.name2.probability = 0.0\nsimianarmy.chaos.TYPE_B.name3.probability = 0.0"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/notificationEnabled.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true\nsimianarmy.chaos.TYPE_B.enabled = true\nsimianarmy.chaos.TYPE_A.name0.notification.enabled = true\nsimianarmy.chaos.TYPE_A.name1.notification.enabled = true\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/ondemandTermination.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\n\nsimianarmy.chaos.terminateOndemand.enabled = true"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/ondemandTerminationDisabled.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\n\nsimianarmy.chaos.terminateOndemand.enabled = false"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/propertiesWithDefaults.properties",
    "content": "simianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true\nsimianarmy.chaos.TYPE_A.probability = 1.0\nsimianarmy.chaos.TYPE_A.maxTerminationsPerDay = 2.0\nsimianarmy.chaos.TYPE_A.named1.enabled = false\nsimianarmy.chaos.TYPE_A.named1.probability = 1.1\nsimianarmy.chaos.TYPE_A.named1.maxTerminationsPerDay = 2.1\n"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsBiggerThanOne.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 1.0\n\nsimianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 2"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsNegative.properties",
    "content": "simianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 1.0\n\nsimianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = -1.0"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsOne.properties",
    "content": "simianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 1.0\n\nsimianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 1.0"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsSmallerThanOne.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 1.0\n\nsimianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 0.5"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsVerySmall.properties",
    "content": "simianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 1.0\n\nsimianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 0.0005"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsZero.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 1.0\n\nsimianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 0"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayGroupLevel.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_C.name4.enabled = true\nsimianarmy.chaos.TYPE_C.name4.probability = 1.0\n\nsimianarmy.chaos.TYPE_C.maxTerminationsPerDay = 3.0"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/unleashedEnabledA.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = true"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/chaos/unleashedEnabledB.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.chaos.leashed = false\nsimianarmy.chaos.TYPE_A.enabled = false\nsimianarmy.chaos.TYPE_B.enabled = true"
  },
  {
    "path": "src/test/resources/com/netflix/simianarmy/resources/chaos/getChaosEventsResponse.json",
    "content": "[{\"monkeyType\":\"CHAOS\",\"eventId\":\"i-123456789012345670\",\"eventType\":\"CHAOS_TERMINATION\",\"eventTime\":1330538400000,\"region\":\"region\",\"groupType\":\"ASG\",\"groupName\":\"testGroup\"},{\"monkeyType\":\"CHAOS\",\"eventId\":\"i-123456789012345671\",\"eventType\":\"CHAOS_TERMINATION\",\"eventTime\":1330538400000,\"region\":\"region\",\"groupType\":\"ASG\",\"groupName\":\"testGroup\"}]"
  },
  {
    "path": "src/test/resources/log4j.properties",
    "content": "log4j.rootLogger=ERROR, stdout\n\n# stdout\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} - %-5p %C{1} - [%F:%L] %m%n\n"
  },
  {
    "path": "src/test/resources/proxy.properties",
    "content": "# Proxy configuration for the purpose of testing\nsimianarmy.client.aws.proxyHost=127.0.0.1\nsimianarmy.client.aws.proxyPort=80\nsimianarmy.client.aws.proxyUser=fakeUser\nsimianarmy.client.aws.proxyPassword=fakePassword\n"
  },
  {
    "path": "src/test/resources/simianarmy.properties",
    "content": "simianarmy.chaos.enabled = true\nsimianarmy.calendar.isMonkeyTime = true\n"
  }
]