Repository: Netflix/SimianArmy Branch: master Commit: e8eb9f3bad23 Files: 289 Total size: 1.3 MB Directory structure: gitextract_54dtl7c9/ ├── .gitignore ├── .netflixoss ├── .travis.yml ├── CHANGELOG.md ├── GNUmakefile ├── LICENSE ├── OSSMETADATA ├── README.md ├── build.gradle ├── buildViaTravis.sh ├── codequality/ │ ├── checkstyle.xml │ └── org.eclipse.jdt.core.prefs ├── gradle/ │ └── wrapper/ │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── installViaTravis.sh ├── settings.gradle └── src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── netflix/ │ │ └── simianarmy/ │ │ ├── AbstractEmailBuilder.java │ │ ├── CloudClient.java │ │ ├── EmailBuilder.java │ │ ├── EventType.java │ │ ├── FeatureNotEnabledException.java │ │ ├── GroupType.java │ │ ├── InstanceGroupNotFoundException.java │ │ ├── Monkey.java │ │ ├── MonkeyCalendar.java │ │ ├── MonkeyConfiguration.java │ │ ├── MonkeyEmailNotifier.java │ │ ├── MonkeyRecorder.java │ │ ├── MonkeyRunner.java │ │ ├── MonkeyScheduler.java │ │ ├── MonkeyType.java │ │ ├── NamedType.java │ │ ├── NotFoundException.java │ │ ├── Resource.java │ │ ├── ResourceType.java │ │ ├── aws/ │ │ │ ├── AWSEmailNotifier.java │ │ │ ├── AWSResource.java │ │ │ ├── AWSResourceType.java │ │ │ ├── RDSRecorder.java │ │ │ ├── STSAssumeRoleSessionCredentialsProvider.java │ │ │ ├── SimpleDBRecorder.java │ │ │ ├── conformity/ │ │ │ │ ├── RDSConformityClusterTracker.java │ │ │ │ ├── SimpleDBConformityClusterTracker.java │ │ │ │ ├── crawler/ │ │ │ │ │ └── AWSClusterCrawler.java │ │ │ │ └── rule/ │ │ │ │ ├── BasicConformityEurekaClient.java │ │ │ │ ├── ConformityEurekaClient.java │ │ │ │ ├── CrossZoneLoadBalancing.java │ │ │ │ ├── InstanceHasHealthCheckUrl.java │ │ │ │ ├── InstanceHasStatusUrl.java │ │ │ │ ├── InstanceInSecurityGroup.java │ │ │ │ ├── InstanceInVPC.java │ │ │ │ ├── InstanceIsHealthyInEureka.java │ │ │ │ ├── InstanceTooOld.java │ │ │ │ └── SameZonesInElbAndAsg.java │ │ │ └── janitor/ │ │ │ ├── ASGJanitor.java │ │ │ ├── EBSSnapshotJanitor.java │ │ │ ├── EBSVolumeJanitor.java │ │ │ ├── ELBJanitor.java │ │ │ ├── ImageJanitor.java │ │ │ ├── InstanceJanitor.java │ │ │ ├── LaunchConfigJanitor.java │ │ │ ├── RDSJanitorResourceTracker.java │ │ │ ├── SimpleDBJanitorResourceTracker.java │ │ │ ├── VolumeTaggingMonkey.java │ │ │ ├── crawler/ │ │ │ │ ├── ASGJanitorCrawler.java │ │ │ │ ├── AbstractAWSJanitorCrawler.java │ │ │ │ ├── EBSSnapshotJanitorCrawler.java │ │ │ │ ├── EBSVolumeJanitorCrawler.java │ │ │ │ ├── ELBJanitorCrawler.java │ │ │ │ ├── InstanceJanitorCrawler.java │ │ │ │ ├── LaunchConfigJanitorCrawler.java │ │ │ │ └── edda/ │ │ │ │ ├── EddaASGJanitorCrawler.java │ │ │ │ ├── EddaEBSSnapshotJanitorCrawler.java │ │ │ │ ├── EddaEBSVolumeJanitorCrawler.java │ │ │ │ ├── EddaELBJanitorCrawler.java │ │ │ │ ├── EddaImageJanitorCrawler.java │ │ │ │ ├── EddaInstanceJanitorCrawler.java │ │ │ │ ├── EddaLaunchConfigJanitorCrawler.java │ │ │ │ └── EddaUtils.java │ │ │ └── rule/ │ │ │ ├── ami/ │ │ │ │ └── UnusedImageRule.java │ │ │ ├── asg/ │ │ │ │ ├── ASGInstanceValidator.java │ │ │ │ ├── DiscoveryASGInstanceValidator.java │ │ │ │ ├── DummyASGInstanceValidator.java │ │ │ │ ├── OldEmptyASGRule.java │ │ │ │ └── SuspendedASGRule.java │ │ │ ├── elb/ │ │ │ │ └── OrphanedELBRule.java │ │ │ ├── generic/ │ │ │ │ ├── TagValueExclusionRule.java │ │ │ │ └── UntaggedRule.java │ │ │ ├── instance/ │ │ │ │ └── OrphanedInstanceRule.java │ │ │ ├── launchconfig/ │ │ │ │ └── OldUnusedLaunchConfigRule.java │ │ │ ├── snapshot/ │ │ │ │ └── NoGeneratedAMIRule.java │ │ │ └── volume/ │ │ │ ├── DeleteOnTerminationRule.java │ │ │ └── OldDetachedVolumeRule.java │ │ ├── basic/ │ │ │ ├── BasicCalendar.java │ │ │ ├── BasicChaosMonkeyContext.java │ │ │ ├── BasicConfiguration.java │ │ │ ├── BasicMonkeyServer.java │ │ │ ├── BasicRecorderEvent.java │ │ │ ├── BasicScheduler.java │ │ │ ├── BasicSimianArmyContext.java │ │ │ ├── LocalDbRecorder.java │ │ │ ├── calendars/ │ │ │ │ └── BavarianCalendar.java │ │ │ ├── chaos/ │ │ │ │ ├── BasicChaosEmailNotifier.java │ │ │ │ ├── BasicChaosInstanceSelector.java │ │ │ │ ├── BasicChaosMonkey.java │ │ │ │ ├── BasicInstanceGroup.java │ │ │ │ └── CloudFormationChaosMonkey.java │ │ │ ├── conformity/ │ │ │ │ ├── BasicConformityEmailBuilder.java │ │ │ │ ├── BasicConformityMonkey.java │ │ │ │ └── BasicConformityMonkeyContext.java │ │ │ └── janitor/ │ │ │ ├── BasicJanitorEmailBuilder.java │ │ │ ├── BasicJanitorMonkey.java │ │ │ ├── BasicJanitorMonkeyContext.java │ │ │ ├── BasicJanitorRuleEngine.java │ │ │ └── BasicVolumeTaggingMonkeyContext.java │ │ ├── chaos/ │ │ │ ├── BlockAllNetworkTrafficChaosType.java │ │ │ ├── BurnCpuChaosType.java │ │ │ ├── BurnIoChaosType.java │ │ │ ├── ChaosCrawler.java │ │ │ ├── ChaosEmailNotifier.java │ │ │ ├── ChaosInstance.java │ │ │ ├── ChaosInstanceSelector.java │ │ │ ├── ChaosMonkey.java │ │ │ ├── ChaosType.java │ │ │ ├── DetachVolumesChaosType.java │ │ │ ├── FailDnsChaosType.java │ │ │ ├── FailDynamoDbChaosType.java │ │ │ ├── FailEc2ChaosType.java │ │ │ ├── FailS3ChaosType.java │ │ │ ├── FillDiskChaosType.java │ │ │ ├── KillProcessesChaosType.java │ │ │ ├── NetworkCorruptionChaosType.java │ │ │ ├── NetworkLatencyChaosType.java │ │ │ ├── NetworkLossChaosType.java │ │ │ ├── NullRouteChaosType.java │ │ │ ├── ScriptChaosType.java │ │ │ ├── ShutdownInstanceChaosType.java │ │ │ └── SshConfig.java │ │ ├── client/ │ │ │ ├── MonkeyRestClient.java │ │ │ ├── aws/ │ │ │ │ ├── AWSClient.java │ │ │ │ └── chaos/ │ │ │ │ ├── ASGChaosCrawler.java │ │ │ │ ├── FilteringChaosCrawler.java │ │ │ │ └── TagPredicate.java │ │ │ ├── edda/ │ │ │ │ └── EddaClient.java │ │ │ └── vsphere/ │ │ │ ├── PropertyBasedTerminationStrategy.java │ │ │ ├── TerminationStrategy.java │ │ │ ├── VSphereClient.java │ │ │ ├── VSphereContext.java │ │ │ ├── VSphereGroups.java │ │ │ └── VSphereServiceConnection.java │ │ ├── conformity/ │ │ │ ├── AutoScalingGroup.java │ │ │ ├── Cluster.java │ │ │ ├── ClusterCrawler.java │ │ │ ├── Conformity.java │ │ │ ├── ConformityClusterTracker.java │ │ │ ├── ConformityEmailBuilder.java │ │ │ ├── ConformityEmailNotifier.java │ │ │ ├── ConformityMonkey.java │ │ │ ├── ConformityRule.java │ │ │ └── ConformityRuleEngine.java │ │ ├── janitor/ │ │ │ ├── AbstractJanitor.java │ │ │ ├── DryRunnableJanitor.java │ │ │ ├── DryRunnableJanitorException.java │ │ │ ├── Janitor.java │ │ │ ├── JanitorCrawler.java │ │ │ ├── JanitorEmailBuilder.java │ │ │ ├── JanitorEmailNotifier.java │ │ │ ├── JanitorMonkey.java │ │ │ ├── JanitorResourceTracker.java │ │ │ ├── JanitorRuleEngine.java │ │ │ └── Rule.java │ │ ├── resources/ │ │ │ ├── chaos/ │ │ │ │ └── ChaosMonkeyResource.java │ │ │ └── janitor/ │ │ │ └── JanitorMonkeyResource.java │ │ └── tunable/ │ │ ├── TunableInstanceGroup.java │ │ └── TunablyAggressiveChaosMonkey.java │ ├── resources/ │ │ ├── chaos.properties │ │ ├── client.properties │ │ ├── conformity.properties │ │ ├── janitor.properties │ │ ├── log4j.properties │ │ ├── scripts/ │ │ │ ├── burncpu.sh │ │ │ ├── burnio.sh │ │ │ ├── faildns.sh │ │ │ ├── faildynamodb.sh │ │ │ ├── failec2.sh │ │ │ ├── fails3.sh │ │ │ ├── filldisk.sh │ │ │ ├── killprocesses.sh │ │ │ ├── networkcorruption.sh │ │ │ ├── networklatency.sh │ │ │ ├── networkloss.sh │ │ │ └── nullroute.sh │ │ ├── simianarmy.properties │ │ └── volumeTagging.properties │ └── webapp/ │ └── WEB-INF/ │ └── web.xml └── test/ ├── java/ │ └── com/ │ └── netflix/ │ └── simianarmy/ │ ├── TestMonkey.java │ ├── TestMonkeyContext.java │ ├── TestMonkeyRunner.java │ ├── TestUtils.java │ ├── aws/ │ │ ├── TestAWSEmailNotifier.java │ │ ├── TestRDSRecorder.java │ │ ├── TestSimpleDBRecorder.java │ │ ├── conformity/ │ │ │ ├── TestASGOwnerEmailTag.java │ │ │ ├── TestRDSConformityClusterTracker.java │ │ │ └── rule/ │ │ │ └── TestInstanceInVPC.java │ │ └── janitor/ │ │ ├── TestAWSResource.java │ │ ├── TestRDSJanitorResourceTracker.java │ │ ├── TestSimpleDBJanitorResourceTracker.java │ │ ├── crawler/ │ │ │ ├── TestASGJanitorCrawler.java │ │ │ ├── TestEBSSnapshotJanitorCrawler.java │ │ │ ├── TestEBSVolumeJanitorCrawler.java │ │ │ ├── TestELBJanitorCrawler.java │ │ │ ├── TestInstanceJanitorCrawler.java │ │ │ └── TestLaunchConfigJanitorCrawler.java │ │ └── rule/ │ │ ├── TestMonkeyCalendar.java │ │ ├── asg/ │ │ │ ├── TestOldEmptyASGRule.java │ │ │ └── TestSuspendedASGRule.java │ │ ├── elb/ │ │ │ └── TestOrphanedELBRule.java │ │ ├── generic/ │ │ │ ├── TestTagValueExclusionRule.java │ │ │ └── TestUntaggedRule.java │ │ ├── instance/ │ │ │ └── TestOrphanedInstanceRule.java │ │ ├── launchconfig/ │ │ │ └── TestOldUnusedLaunchConfigRule.java │ │ ├── snapshot/ │ │ │ └── TestNoGeneratedAMIRule.java │ │ └── volume/ │ │ └── TestOldDetachedVolumeRule.java │ ├── basic/ │ │ ├── TestBasicCalendar.java │ │ ├── TestBasicConfiguration.java │ │ ├── TestBasicContext.java │ │ ├── TestBasicMonkeyServer.java │ │ ├── TestBasicRecorderEvent.java │ │ ├── TestBasicScheduler.java │ │ ├── calendar/ │ │ │ └── TestBavarianCalendar.java │ │ ├── chaos/ │ │ │ ├── TestBasicChaosEmailNotifier.java │ │ │ ├── TestBasicChaosInstanceSelector.java │ │ │ ├── TestBasicChaosMonkey.java │ │ │ └── TestCloudFormationChaosMonkey.java │ │ └── janitor/ │ │ └── TestBasicJanitorRuleEngine.java │ ├── chaos/ │ │ ├── TestChaosMonkeyArmy.java │ │ └── TestChaosMonkeyContext.java │ ├── client/ │ │ ├── aws/ │ │ │ ├── TestAWSClient.java │ │ │ └── chaos/ │ │ │ ├── TestASGChaosCrawler.java │ │ │ └── TestFilterASGChaosCrawler.java │ │ └── vsphere/ │ │ ├── TestPropertyBasedTerminationStrategy.java │ │ ├── TestVSpehereClient.java │ │ ├── TestVSphereContext.java │ │ ├── TestVSphereGroups.java │ │ └── TestVSphereServiceConnection.java │ ├── conformity/ │ │ ├── TestCrossZoneLoadBalancing.java │ │ └── TestSameZonesInElbAndAsg.java │ ├── janitor/ │ │ ├── TestAbstractJanitor.java │ │ └── TestBasicJanitorMonkeyContext.java │ ├── resources/ │ │ └── chaos/ │ │ └── TestChaosMonkeyResource.java │ └── tunable/ │ └── TestTunablyAggressiveChaosMonkey.java └── resources/ ├── chaos.properties ├── client.properties ├── com/ │ └── netflix/ │ └── simianarmy/ │ ├── chaos/ │ │ ├── all.properties │ │ ├── cloudformation.properties │ │ ├── disabled.properties │ │ ├── enabledA.properties │ │ ├── enabledAwith0.properties │ │ ├── enabledAwithout1.properties │ │ ├── enabledB.properties │ │ ├── fullProbability.properties │ │ ├── globalNotificationEnabled.properties │ │ ├── mandatoryTerminationDisabled.properties │ │ ├── mandatoryTerminationInsideWindow.properties │ │ ├── mandatoryTerminationNoOptInTime.properties │ │ ├── mandatoryTerminationNotDefined.properties │ │ ├── mandatoryTerminationOutsideWindow.properties │ │ ├── noProbability.properties │ │ ├── noProbabilityByName.properties │ │ ├── notificationEnabled.properties │ │ ├── ondemandTermination.properties │ │ ├── ondemandTerminationDisabled.properties │ │ ├── propertiesWithDefaults.properties │ │ ├── terminationPerDayAsBiggerThanOne.properties │ │ ├── terminationPerDayAsNegative.properties │ │ ├── terminationPerDayAsOne.properties │ │ ├── terminationPerDayAsSmallerThanOne.properties │ │ ├── terminationPerDayAsVerySmall.properties │ │ ├── terminationPerDayAsZero.properties │ │ ├── terminationPerDayGroupLevel.properties │ │ ├── unleashedEnabledA.properties │ │ └── unleashedEnabledB.properties │ └── resources/ │ └── chaos/ │ └── getChaosEventsResponse.json ├── log4j.properties ├── proxy.properties └── simianarmy.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled source # ################### *.com *.class *.dll *.exe *.o *.so # Packages # ############ # it's better to unpack these files and commit the raw source # git has its own built in compression methods *.7z *.dmg *.gz *.iso *.jar *.rar *.tar *.zip # Logs and databases # ###################### *.log # OS generated files # ###################### .DS_Store* ehthumbs.db Icon? Thumbs.db # Editor Files # ################ *~ *.swp # Gradle Files # ################ .gradle # Build output directies /target */target /build */build /bin */bin /test-output */test-output # IntelliJ specific files/directories out .idea *.ipr *.iws *.iml atlassian-ide-plugin.xml # Eclipse specific files/directories .classpath .project .settings .metadata # NetBeans specific files/directories .nbattrs /bin /test-output ================================================ FILE: .netflixoss ================================================ cloudbees_disabled=true ================================================ FILE: .travis.yml ================================================ language: java jdk: - oraclejdk8 sudo: required dist: trusty install: ./installViaTravis.sh script: ./buildViaTravis.sh cache: directories: - $HOME/.gradle/caches env: global: - secure: WCRqvIKdPdIsoDhsJWZNBZhEH7Jdgz2fmkjzozVjs4dq36ySrH71udNtZcPIsTwjmHpRaGX0XCgmwLC5WorS2TBJJ87oghCP3WWQGMBLcCdXHS8quRdAHLHpNfao/BQrBEA/gmCYoJZdmXKFDc+XKXS5NBrHkkvVfLGCumcP0AI= - secure: TKnGiZyCtWWI/ei2lNDvGIjyAI4W8xMNOlXT6tGiWJgexvFQpTl2NgkMqgwbxReyxj37vdUnn9Lb/883G6zL/uB+l5aCjeCG//6GAbJYdrSZQCE/UCo7iMlAxyqfuIlKcJABIhwpP8Fg4RwqxJG19Tbx5ddg8RP8yKAi1QNx06Y= - secure: nUn8s+1fV60Hxb9V9DouFIOGHeBpeTD7l6Yadw4gthvi/tZndZ+L/Crh1Z9pAU69NqEHG/VcFLUMNER7dQ4rugVbcbfQueeCdnVpmStLS97tAl8kArhpWCk8dQi47IANuQw7U0nVlg3pA8w9HLZX6ee9PnhyG1oOnluPC/x2Or4= - secure: KTtxnPJWfkwNwYkd2IxKAc4dUc6jF0Fd6uhrqK5q36z0RnY4b/gKlx8bjGPcZA5hutNmiN/gxyvpbL/bvVg9buQ2vkybaPZpzpLwhHTXiD5accjQUMuwF8DFYpzIb104hkgzHbrW18JRImK539ib5TTanF3I08F04LssSXG8NnY= ================================================ FILE: CHANGELOG.md ================================================ ================================================ FILE: GNUmakefile ================================================ reformat: eclipse -nosplash -application org.eclipse.jdt.core.JavaCodeFormatter -verbose -config $(shell pwd)/codequality/org.eclipse.jdt.core.prefs $(shell pwd)/src find $(shell pwd)/src -name \*.java | xargs perl -pi -e 's/{ /{/g; s/(\S) }/$$1}/g; s/\* $$/\*/; s/([.]<[^>]+>)\s+/$$1/g' ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2012 Netflix, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: OSSMETADATA ================================================ osslifecycle=archived ================================================ FILE: README.md ================================================ [![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/SimianArmy.svg)](OSSMETADATA) [![Build Status](https://travis-ci.org/Netflix/SimianArmy.svg?branch=master)](https://travis-ci.org/Netflix/SimianArmy) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ## PROJECT STATUS: RETIRED **The Simian Army project is no longer actively maintained**. Some of the Simian Army functionality has been moved to other Netflix projects: * A [newer version of Chaos Monkey](https://github.com/netflix/chaosmonkey) is available as a standalone service. * [Swabbie] is a new standalone service that will replace the functionality provided by Janitor Monkey. * Conformity Monkey functionality will be rolled into other [Spinnaker] backend services. [Swabbie]: https://github.com/spinnaker/swabbie [Spinnaker]: https://www.spinnaker.io/ ### DESCRIPTION The Simian Army is a suite of tools for keeping your cloud operating in top form. Chaos Monkey, the first member, is a resiliency tool that helps ensure that your applications can tolerate random instance failures ### DETAILS Please see the [wiki](https://github.com/Netflix/SimianArmy/wiki). ### SUPPORT [Simian Army Google group](http://groups.google.com/group/simianarmy-users) Because the project is no longer maintained, there is a good chance that nobody will be able to answer a support question. ### LICENSE Copyright 2012-2016 Netflix, Inc. Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: build.gradle ================================================ buildscript { repositories { jcenter() } } plugins { id 'nebula.netflixoss' version '3.2.3' id 'net.saliman.cobertura' version '2.2.7' id 'com.github.hierynomus.license' version '0.11.0' } // Establish version and status ext.githubProjectName = 'SimianArmy' group = "com.netflix.${project.name}" apply plugin:'eclipse-wtp' repositories { mavenLocal() mavenCentral() } apply plugin: 'war' apply plugin: 'jetty' sourceCompatibility = 1.8 targetCompatibility = 1.8 dependencies { // for the VMWareClient compile 'com.cloudbees.thirdparty:vijava:5.0.0' // for DB support outside of AWS (SimpleDB not available) compile 'org.mapdb:mapdb:0.9.5' compile 'com.sun.jersey:jersey-servlet:1.19' compile 'org.slf4j:slf4j-api:1.7.2' compile 'org.codehaus.jackson:jackson-core-asl:1.9.2' compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.2' compile 'com.netflix.eureka:eureka-client:1.4.1' compile 'com.amazonaws:aws-java-sdk:1.11.28' compile 'commons-lang:commons-lang:2.6' compile 'com.google.guava:guava:11.0.2' compile 'org.apache.httpcomponents:httpclient:4.3' compile 'com.google.auto.service:auto-service:1.0-rc2' compile 'org.apache.jclouds.driver:jclouds-jsch:1.9.0' compile 'org.apache.jclouds.driver:jclouds-slf4j:1.9.0' compile 'org.apache.jclouds.api:ec2:1.9.0' compile 'org.apache.jclouds.provider:aws-ec2:1.9.0' compile 'com.netflix.servo:servo-core:0.12.11' compile 'org.springframework:spring-jdbc:4.2.5.RELEASE' compile 'com.zaxxer:HikariCP:2.4.7' testCompile 'org.testng:testng:6.3.1' testCompile 'org.mockito:mockito-core:1.8.5' runtime 'org.slf4j:slf4j-log4j12:1.6.1' providedCompile 'javax.servlet:servlet-api:2.5' } test { useTestNG() } tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint" } license { exclude '**/*.properties' exclude '**/*.json' exclude '**/*.sh' } task coreJar(type: Jar) { from sourceSets.main.output include '**' } publishing { publications { mavenWar(MavenPublication) { from components.web artifact coreJar { classifier "core" } } } } artifactoryPublish { publications('mavenWar') } ================================================ FILE: buildViaTravis.sh ================================================ #!/bin/bash # This script will build the project. if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then echo -e "Build Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]" ./gradlew clean build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' case "$TRAVIS_TAG" in *-rc\.*) ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" candidate ;; *) ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" final ;; esac else echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']' ./gradlew build fi ================================================ FILE: codequality/checkstyle.xml ================================================ ================================================ FILE: codequality/org.eclipse.jdt.core.prefs ================================================ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 org.eclipse.jdt.core.formatter.alignment_for_assignment=0 org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 org.eclipse.jdt.core.formatter.blank_lines_before_field=0 org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 org.eclipse.jdt.core.formatter.blank_lines_before_method=1 org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 org.eclipse.jdt.core.formatter.blank_lines_before_package=0 org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false org.eclipse.jdt.core.formatter.comment.format_block_comments=true org.eclipse.jdt.core.formatter.comment.format_header=false org.eclipse.jdt.core.formatter.comment.format_html=true org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true org.eclipse.jdt.core.formatter.comment.format_line_comments=true org.eclipse.jdt.core.formatter.comment.format_source_code=true org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true org.eclipse.jdt.core.formatter.comment.indent_root_tags=true org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert org.eclipse.jdt.core.formatter.comment.line_length=120 org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false org.eclipse.jdt.core.formatter.compact_else_if=true org.eclipse.jdt.core.formatter.continuation_indentation=2 org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true org.eclipse.jdt.core.formatter.indent_empty_lines=false org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false org.eclipse.jdt.core.formatter.indentation.size=4 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.join_lines_in_comments=true org.eclipse.jdt.core.formatter.join_wrapped_lines=true org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false org.eclipse.jdt.core.formatter.lineSplit=120 org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true org.eclipse.jdt.core.formatter.tabulation.char=space org.eclipse.jdt.core.formatter.tabulation.size=4 org.eclipse.jdt.core.formatter.use_on_off_tags=false org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Feb 22 15:12:15 PST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip ================================================ FILE: gradle.properties ================================================ ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: installViaTravis.sh ================================================ #!/bin/bash # This script will build the project. if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then echo -e "Assemble Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]" ./gradlew clean assemble --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Assemble Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' ./gradlew -Prelease.travisci=true assemble elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Assemble Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true assemble else echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']' ./gradlew assemble fi ================================================ FILE: settings.gradle ================================================ rootProject.name='simianarmy' ================================================ FILE: src/main/java/com/netflix/simianarmy/AbstractEmailBuilder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** The abstract email builder. */ public abstract class AbstractEmailBuilder implements EmailBuilder { @Override public String buildEmailBody(String emailAddress) { StringBuilder body = new StringBuilder(); String header = getHeader(); if (header != null) { body.append(header); } String entryTable = getEntryTable(emailAddress); if (entryTable != null) { body.append(entryTable); } String footer = getFooter(); if (footer != null) { body.append(footer); } return body.toString(); } /** * Gets the header to the email body. */ protected abstract String getHeader(); /** * Gets the table of entries in the email body. * @param emailAddress the email address to notify * @return the HTML string representing the table for the resources to send to the * email address */ protected abstract String getEntryTable(String emailAddress); /** * Gets the footer of the email body. */ protected abstract String getFooter(); /** * Gets the HTML cell in the table of a string value. * @param value the string to put in the table * @return the HTML text */ protected String getHtmlCell(String value) { return "" + value + ""; } /** * Gets the HTML string displaying the table header with the specified column names. * @param columns the column names for the table */ protected String getHtmlTableHeader(String[] columns) { StringBuilder tableHeader = new StringBuilder(); tableHeader.append( ""); tableHeader.append(""); for (String col : columns) { tableHeader.append(getHtmlCell(col)); } tableHeader.append(""); return tableHeader.toString(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/CloudClient.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import org.jclouds.compute.ComputeService; import org.jclouds.domain.LoginCredentials; import org.jclouds.ssh.SshClient; import java.util.List; import java.util.Map; /** * The CloudClient interface. This abstractions provides the interface that the monkeys need to interact with * "the cloud". */ public interface CloudClient { /** * Terminates instance. * * @param instanceId * the instance id * * @throws NotFoundException * if the instance no longer exists or was already terminated after the crawler discovered it then you * should get a NotFoundException */ void terminateInstance(String instanceId); /** * Deletes an auto scaling group. * * @param asgName * the auto scaling group name */ void deleteAutoScalingGroup(String asgName); /** * Deletes a launch configuration. * * @param launchConfigName * the launch configuration name */ void deleteLaunchConfiguration(String launchConfigName); /** * Deletes a volume. * * @param volumeId * the volume id */ void deleteVolume(String volumeId); /** * Deletes a snapshot. * * @param snapshotId * the snapshot id. */ void deleteSnapshot(String snapshotId); /** Deletes an image. * * @param imageId * the image id. */ void deleteImage(String imageId); /** * Deletes an elastic load balancer. * * @param elbId * the elastic load balancer id */ void deleteElasticLoadBalancer(String elbId); /** * Deletes a DNS record. * * @param dnsName * the DNS record to delete * @param dnsType * the DNS type (CNAME, A, or AAAA) * @param hostedZoneID * the ID of the hosted zone (required for AWS Route53 records) */ public void deleteDNSRecord(String dnsName, String dnsType, String hostedZoneID); /** * Adds or overwrites tags for the specified resources. * * @param keyValueMap * the new tags in the form of map from key to value * * @param resourceIds * the list of resource ids */ void createTagsForResources(Map keyValueMap, String... resourceIds); /** * Lists all EBS volumes attached to the specified instance. * * @param instanceId * the instance id * @param includeRoot * if the root volume is on EBS, should we include it? * * @throws NotFoundException * if the instance no longer exists or was already terminated after the crawler discovered it then you * should get a NotFoundException */ List listAttachedVolumes(String instanceId, boolean includeRoot); /** * Detaches an EBS volumes from the specified instance. * * @param instanceId * the instance id * @param volumeId * the volume id * @param force * if we should force-detach the volume. Probably best not to use on high-value volumes. * * @throws NotFoundException * if the instance no longer exists or was already terminated after the crawler discovered it then you * should get a NotFoundException */ void detachVolume(String instanceId, String volumeId, boolean force); /** * Returns the jClouds compute service. */ ComputeService getJcloudsComputeService(); /** * Returns the jClouds node id for an instance id on this CloudClient. */ String getJcloudsId(String instanceId); /** * Opens an SSH connection to an instance. * * @param instanceId * instance id to connect to * @param credentials * SSH credentials to use * @return {@link SshClient}, in connected state */ SshClient connectSsh(String instanceId, LoginCredentials credentials); /** * Finds a security group with the given name, that can be applied to the given instance. * * For example, if it is a VPC instance, it makes sure that it is in the same VPC group. * * @param instanceId * the instance that the group must be applied to * @param groupName * the name of the group to find * * @return The group id, or null if not found */ String findSecurityGroup(String instanceId, String groupName); /** * Creates an (empty) security group, that can be applied to the given instance. * * @param instanceId * instance that group should be applicable to * @param groupName * name for new group * @param description * description for new group * * @return the id of the security group */ String createSecurityGroup(String instanceId, String groupName, String description); /** * Checks if we can change the security groups of an instance. * * @param instanceId * instance to check * * @return true iff we can change security groups. */ boolean canChangeInstanceSecurityGroups(String instanceId); /** * Sets the security groups for an instance. * * Note this is only valid for VPC instances. * * @param instanceId * the instance id * @param groupIds * ids of desired new groups * * @throws NotFoundException * if the instance no longer exists or was already terminated after the crawler discovered it then you * should get a NotFoundException */ void setInstanceSecurityGroups(String instanceId, List groupIds); } ================================================ FILE: src/main/java/com/netflix/simianarmy/EmailBuilder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** Interface for build the email body. */ public interface EmailBuilder { /** * Builds an email body for an email address. * @param emailAddress the email address to send notification to * @return the email body */ String buildEmailBody(String emailAddress); } ================================================ FILE: src/main/java/com/netflix/simianarmy/EventType.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * Marker interface for all event type enumerations. */ public interface EventType extends NamedType { } ================================================ FILE: src/main/java/com/netflix/simianarmy/FeatureNotEnabledException.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * The Class FeatureNotEnabledException. * * These exceptions will be thrown when a feature is not enabled when being accessed. */ public class FeatureNotEnabledException extends Exception { private static final long serialVersionUID = 8392434473284901306L; /** * Instantiates a FeatureNotEnabledException with a message. * @param msg the error message */ public FeatureNotEnabledException(String msg) { super(msg); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/GroupType.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * Marker interface for all group type enumerations. */ public interface GroupType extends NamedType { } ================================================ FILE: src/main/java/com/netflix/simianarmy/InstanceGroupNotFoundException.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * The Class InstanceGroupNotFoundException. * * These exceptions will be thrown when an instance group cannot be found with the * given name and type. */ public class InstanceGroupNotFoundException extends Exception { private static final long serialVersionUID = -5492120875166280476L; private final String groupType; private final String groupName; /** * Instantiates an InstanceGroupNotFoundException with the group type and name. * @param groupType the group type * @param groupName the gruop name */ public InstanceGroupNotFoundException(String groupType, String groupName) { super(errorMessage(groupType, groupName)); this.groupType = groupType; this.groupName = groupName; } @Override public String toString() { return errorMessage(groupType, groupName); } private static String errorMessage(String groupType, String groupName) { return String.format("Instance group named '%s' [type %s] cannot be found.", groupName, groupType); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/Monkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.MonkeyRecorder.Event; /** * The abstract Monkey class, it provides a minimal interface from which all monkeys must be derived. */ public abstract class Monkey { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(Monkey.class); /** * The Interface Context. */ public interface Context { /** * Scheduler. * * @return the monkey scheduler */ MonkeyScheduler scheduler(); /** * Calendar. * * @return the monkey calendar */ MonkeyCalendar calendar(); /** * Cloud client. * * @return the cloud client */ CloudClient cloudClient(); /** * Recorder. * * @return the monkey recorder */ MonkeyRecorder recorder(); /** * Add a event to the summary report. The ChaosMonkey uses this to print a summary after the chaos run is * complete. * * @param evt * The Event to be reported */ void reportEvent(Event evt); /** * Used to clear the event summary on the start of a chaos run. */ void resetEventReport(); /** * Returns a summary of what the chaos run did. */ String getEventReport(); /** * Configuration. * * @return the monkey configuration */ MonkeyConfiguration configuration(); } /** The context. */ private final Context ctx; /** * Instantiates a new monkey. * * @param ctx * the context */ public Monkey(Context ctx) { this.ctx = ctx; } /** * Type. * * @return the monkey type enum */ public abstract MonkeyType type(); /** * Do monkey business. */ public abstract void doMonkeyBusiness(); /** * Context. * * @return the context */ public Context context() { return ctx; } /** * Run. This is run on the schedule set by the MonkeyScheduler */ public void run() { if (ctx.calendar().isMonkeyTime(this)) { LOGGER.info(this.type().name() + " Monkey Running ..."); try { this.doMonkeyBusiness(); } finally { String eventReport = context().getEventReport(); if (eventReport != null) { LOGGER.info("Reporting what I did...\n" + eventReport); } } } else { LOGGER.info("Not Time for " + this.type().name() + " Monkey"); } } /** * Start. Sets up the schedule for the monkey to run on. */ public void start() { final Monkey me = this; ctx.scheduler().start(this, new Runnable() { @Override public void run() { try { me.run(); } catch (Exception e) { LOGGER.error(me.type().name() + " Monkey Error: ", e); } } }); } /** * Stop. Removes the monkey from the schedule. */ public void stop() { ctx.scheduler().stop(this); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/MonkeyCalendar.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import java.util.Calendar; import java.util.Date; /** * The Interface MonkeyCalendar used to tell if a monkey should be running or now. We only want monkeys to run during * business hours, so that engineers will be on-hand if something goes wrong. */ public interface MonkeyCalendar { /** * Checks if is monkey time. * * @param monkey * the monkey * @return true, if is monkey time */ boolean isMonkeyTime(Monkey monkey); /** * Open hour. This is the "open" hour for then the monkey should start working. * * @return the int */ int openHour(); /** * Close hour. This is the "close" hour for when the monkey should stop working. * * @return the int */ int closeHour(); /** * Get the current time using whatever timezone is used for monkey date calculations. * * @return the calendar */ Calendar now(); /** Gets the next business day from the start date after n business days. * * @param date the start date * @param n the number of business days from now * @return the business day after n business days */ Date getBusinessDay(Date date, int n); } ================================================ FILE: src/main/java/com/netflix/simianarmy/MonkeyConfiguration.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * The Interface MonkeyConfiguration. */ public interface MonkeyConfiguration { /** * Gets the boolean associated with property string. If not found it will return false. * * @param property * the property name * @return the boolean value */ boolean getBool(String property); /** * Gets the boolean associated with property string. If not found it will return dflt. * * @param property * the property name * @param dflt * the default value * @return the bool property value, or dflt if none set */ boolean getBoolOrElse(String property, boolean dflt); /** * Gets the number (double) associated with property string. If not found it will return dflt. * * @param property * the property name * @param dflt * the default value * @return the numeric property value, or dflt if none set */ double getNumOrElse(String property, double dflt); /** * Gets the string associated with property string. If not found it will return null. * * @param property * the property name * @return the string property value */ String getStr(String property); /** * Gets the string associated with property string. If not found it will return dflt. * * @param property * the property name * @param dflt * the default value * @return the string property value, or dflt if none set */ String getStrOrElse(String property, String dflt); /** * If the configuration has dynamic elements then they should be reloaded with this. */ void reload(); /** * Reloads the properties of specific group. * @param groupName * the instance group's name */ void reload(String groupName); } ================================================ FILE: src/main/java/com/netflix/simianarmy/MonkeyEmailNotifier.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** The interface for the email notifier used by monkeys. */ public interface MonkeyEmailNotifier { /** * Determines if a email address is valid. * @param email the email * @return true if the email address is valid, false otherwise. */ boolean isValidEmail(String email); /** * Builds an email subject for an email address. * @param to the destination email address * @return the email subject */ String buildEmailSubject(String to); /** * Gets the cc email addresses for a to address. * @param to the to address * @return the cc email addresses */ String[] getCcAddresses(String to); /** * Gets the source email addresses for a to address. * @param to the to address * @return the source email addresses */ String getSourceAddress(String to); /** * Sends an email. * @param to the address sent to * @param subject the email subject * @param body the email body */ void sendEmail(String to, String subject, String body); } ================================================ FILE: src/main/java/com/netflix/simianarmy/MonkeyRecorder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import java.util.Date; import java.util.List; import java.util.Map; /** * The Interface MonkeyRecorder. This is use to store and find events in some datastore. */ public interface MonkeyRecorder { /** * The Interface Event. */ public interface Event { /** * Event Id. * * @return the string */ String id(); /** * Event time. * * @return the date */ Date eventTime(); /** * Monkey type. * * @return the monkey type enum */ MonkeyType monkeyType(); /** * Event type. * * @return the event type enum */ EventType eventType(); /** * Region. * * @return the region for the event */ String region(); /** * Fields. * * * @return the map of strings that may have been provided when the event was created */ Map fields(); /** * Field. * * @param name * the name * @return the string associated with that field */ String field(String name); /** * Adds the field. * * @param name * the name * @param value * the value * @return this so you can chain multiple addField calls together */ Event addField(String name, String value); } /** * New event. * * @param monkeyType * the monkey type * @param eventType * the event type * @param region * the region the event occurred * @param id * the id * @return the event */ Event newEvent(MonkeyType monkeyType, EventType eventType, String region, String id); default Event newEvent(MonkeyType monkeyType, EventType eventType, Resource resource, String id) { if (resource == null) throw new IllegalArgumentException("resource must not be null"); Event event = newEvent(monkeyType, eventType, resource.getRegion(), id); if (resource.getAllTagKeys() != null) { for(String key : resource.getAllTagKeys()) { event.addField(key, resource.getTag(key)); } } event.addField("ResourceDescription", resource.getDescription()); event.addField("ResourceType", resource.getResourceType().toString()); event.addField("ResourceId", resource.getId()); return event; } /** * Record event. * * @param evt * the evt */ void recordEvent(Event evt); /** * Find events. * * @param query * arbitrary map of strings to used to filter the results * @param after * the after * @return the list of events */ List findEvents(Map query, Date after); /** * Find events. * * @param monkeyType * the monkey type * @param query * arbitrary map of strings to used to filter the results * @param after * the after * @return the list of events */ List findEvents(MonkeyType monkeyType, Map query, Date after); /** * Find events. * * @param monkeyType * the monkey type * @param eventType * the event type * @param query * arbitrary map of strings to used to filter the results * @param after * the after * @return the list */ List findEvents(MonkeyType monkeyType, EventType eventType, Map query, Date after); } ================================================ FILE: src/main/java/com/netflix/simianarmy/MonkeyRunner.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import java.lang.reflect.Constructor; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The MonkeyRunner Singleton. */ public enum MonkeyRunner { /** The instance. */ INSTANCE; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(MonkeyRunner.class); /** * Gets the single instance of MonkeyRunner. * * @return single instance of MonkeyRunner */ public static MonkeyRunner getInstance() { return INSTANCE; } /** * Start all the monkeys registered with addMonkey or replaceMonkey. */ public void start() { for (Monkey monkey : monkeys) { LOGGER.info("Starting " + monkey.type().name() + " Monkey"); monkey.start(); } } /** * Stop all of the registered monkeys. */ public void stop() { for (Monkey monkey : monkeys) { LOGGER.info("Stopping " + monkey.type().name() + " Monkey"); monkey.stop(); } } /** * The monkey map. Maps the monkey class to the context class that is registered. This is so we can create new * monkeys in factory() that have the same context types as the registered ones. */ private final Map, Class> monkeyMap = new HashMap, Class>(); /** The monkeys. */ private final List monkeys = new LinkedList(); /** * Gets the registered monkeys. * * @return the monkeys */ public List getMonkeys() { return Collections.unmodifiableList(monkeys); } /** * Adds a simple monkey void constructor. * * @param monkeyClass * the monkey class */ public void addMonkey(Class monkeyClass) { addMonkey(monkeyClass, null); } /** * Replace a simple monkey that has void constructor. * * @param monkeyClass * the monkey class */ public void replaceMonkey(Class monkeyClass) { replaceMonkey(monkeyClass, null); } /** * Adds the monkey. * * @param monkeyClass * the monkey class * @param ctxClass * the context class that is passed to the monkey class constructor. */ public void addMonkey(Class monkeyClass, Class ctxClass) { if (monkeyMap.containsKey(monkeyClass)) { throw new RuntimeException(monkeyClass.getName() + " already registered, use replaceMonkey instead of addMonkey"); } monkeyMap.put(monkeyClass, ctxClass); monkeys.add(factory(monkeyClass, ctxClass)); } /** * Replace monkey. If a monkey is already registered this will replace that registered monkey. * * @param monkeyClass * the monkey class * @param ctxClass * the context class that is passed to the monkey class constructor. */ public void replaceMonkey(Class monkeyClass, Class ctxClass) { monkeyMap.put(monkeyClass, ctxClass); ListIterator li = monkeys.listIterator(); while (li.hasNext()) { Monkey monkey = li.next(); if (monkey.getClass() == monkeyClass) { li.set(factory(monkeyClass, ctxClass)); return; } } Monkey monkey = factory(monkeyClass, ctxClass); monkeys.add(monkey); } /** * Removes the monkey. factory() will no longer be able to construct monkeys of the specified monkey class. * * @param monkeyClass * the monkey class */ public void removeMonkey(Class monkeyClass) { ListIterator li = monkeys.listIterator(); while (li.hasNext()) { Monkey monkey = li.next(); if (monkey.getClass() == monkeyClass) { monkey.stop(); li.remove(); break; } } monkeyMap.remove(monkeyClass); } /** * Monkey factory. This will generate a new monkey object of the monkeyClass type. If a monkey of monkeyClass has * not been registered then this will attempt to find a registered subclass and create an object of that type. * Example: * *
     *         {@code
     *         MonkeyRunner.getInstance().addMonkey(BasicChaosMonkey.class, BasicMonkeyContext.class);
     *         // This will actually return a BasicChaosMonkey since that is the only subclass that was registered
     *         ChaosMonkey monkey = MonkeyRunner.getInstance().factory(ChaosMonkey.class);
     *}
     * 
* * @param * the generic type, must be a subclass of Monkey * @param monkeyClass * the monkey class * @return the monkey */ public T factory(Class monkeyClass) { Class ctxClass = getContextClass(monkeyClass); if (ctxClass == null) { // look for derived class already in our map for (Map.Entry, Class> pair : monkeyMap.entrySet()) { if (monkeyClass.isAssignableFrom(pair.getKey())) { @SuppressWarnings("unchecked") T monkey = (T) factory(pair.getKey(), pair.getValue()); return monkey; } } } return factory(monkeyClass, ctxClass); } /** * Monkey Factory. Given a monkey class and a monkey context class it will generate a new monkey. If the * contextClass is null it will try to generate a new monkeyClass with a void constructor; * * @param * the generic type, must be a subclass of Monkey * @param monkeyClass * the monkey class * @param contextClass * the context class * @return the monkey */ public T factory(Class monkeyClass, Class contextClass) { try { if (contextClass == null) { // assume Monkey class has has void ctor return monkeyClass.newInstance(); } // then find corresponding ctor for (Constructor ctor : monkeyClass.getDeclaredConstructors()) { Class[] paramTypes = ctor.getParameterTypes(); if (paramTypes.length != 1) { continue; } if (paramTypes[0].getName().endsWith("$Context")) { @SuppressWarnings("unchecked") T monkey = (T) ctor.newInstance(contextClass.newInstance()); return monkey; } } } catch (Exception e) { LOGGER.error("monkeyFactory error, cannot make monkey from " + monkeyClass.getName() + " with " + (contextClass == null ? null : contextClass.getName()), e); } return null; } /** * Gets the context class. You should not need this. * * @param monkeyClass * the monkey class * @return the context class or null if a monkeyClass has not been registered */ public Class getContextClass(Class monkeyClass) { return monkeyMap.get(monkeyClass); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/MonkeyScheduler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import java.util.concurrent.TimeUnit; /** * The Interface MonkeyScheduler. */ public interface MonkeyScheduler { /** * Frequency. How often the monkey should run, works in conjunction with frequencyUnit(). If frequency is 2 and * frequencyUnit is TimeUnit.HOUR then the monkey will run once ever 2 hours. * * @return the frequency interval */ int frequency(); /** * Frequency unit. This is the time unit that corresponds with frequency(). * * @return time unit */ TimeUnit frequencyUnit(); /** * Start the scheduler to cause the monkey run at a specified interval. * * @param monkey * the monkey being scheduled * @param run * the Runnable to start, generally calls doMonkeyBusiness */ void start(Monkey monkey, Runnable run); /** * Stop the scheduler for a given monkey. After this the monkey will no longer run on the fixed schedule. * * @param monkey * the monkey being scheduled */ void stop(Monkey monkey); } ================================================ FILE: src/main/java/com/netflix/simianarmy/MonkeyType.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * Marker interface for all monkey type enumerations. */ public interface MonkeyType extends NamedType { } ================================================ FILE: src/main/java/com/netflix/simianarmy/NamedType.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * Interface requiring a name() method. */ public interface NamedType { /** * Name of this instance. */ String name(); } ================================================ FILE: src/main/java/com/netflix/simianarmy/NotFoundException.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * The Class NotFoundException. * * These exceptions will be thrown when a Monkey is trying to interact with a remote resource but it no longer exists * (or never existed). It is used as an adapter to translate a cloud provider exception into something common that the * monkeys can easily handle. */ @SuppressWarnings("serial") public class NotFoundException extends RuntimeException { /** * Instantiates a new NotFound exception. * * @param message * the exception message */ public NotFoundException(String message) { super(message); } /** * Instantiates a new NotFound exception. * * @param message * the exception message * @param cause * the exception cause. This should be the raw exception from the cloud provider. */ public NotFoundException(String message, Throwable cause) { super(message, cause); } /** * Instantiates a new NotFound exception. * * @param cause * the exception cause. This should be the raw exception from the cloud provider. */ public NotFoundException(Throwable cause) { super(cause); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/Resource.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import java.util.Collection; import java.util.Date; import java.util.Map; /** * The interface of Resource. It defines the interfaces for getting the common properties of a resource, as well as * the methods to add and retrieve the additional properties of a resource. Instead of defining a new subclass of * the Resource interface, new resources that have additional fields other than the common ones can be represented, * by adding field-value pairs. This approach makes serialization and deserialization of resources much easier with * the cost of type safety. */ public interface Resource { /** The enum representing the cleanup state of a resource. **/ public enum CleanupState { /** The resource is marked as a cleanup candidate but has not been cleaned up yet. **/ MARKED, /** The resource is terminated by janitor monkey. **/ JANITOR_TERMINATED, /** The resource is terminated by user before janitor monkey performs the termination. **/ USER_TERMINATED, /** The resource is unmarked and not for cleanup anymore due to some change of situations. **/ UNMARKED } /** * Gets the resource id. * * @return the resource id */ String getId(); /** * Sets the resource id. * * @param id the resource id */ void setId(String id); /** * Sets the resource id and returns the resource. * * @param id the resource id * @return the resource object */ Resource withId(String id); /** * Gets the resource type. * * @return the resource type enum */ ResourceType getResourceType(); /** * Sets the resource type. * * @param type the resource type enum */ void setResourceType(ResourceType type); /** * Sets the resource type and returns the resource. * * @param type resource type enum * @return the resource object */ Resource withResourceType(ResourceType type); /** * Gets the region the resource is in. * * @return the region of the resource */ String getRegion(); /** * Sets the region the resource is in. * * @param region the region the resource is in */ void setRegion(String region); /** * Sets the resource region and returns the resource. * * @param region the region the resource is in * @return the resource object */ Resource withRegion(String region); /** * Gets the owner email of the resource. * * @return the owner email of the resource */ String getOwnerEmail(); /** * Sets the owner email of the resource. * * @param ownerEmail the owner email of the resource */ void setOwnerEmail(String ownerEmail); /** * Sets the resource owner email and returns the resource. * * @param ownerEmail the owner email of the resource * @return the resource object */ Resource withOwnerEmail(String ownerEmail); /** * Gets the description of the resource. * * @return the description of the resource */ String getDescription(); /** * Sets the description of the resource. * * @param description the description of the resource */ void setDescription(String description); /** * Sets the resource description and returns the resource. * * @param description the description of the resource * @return the resource object */ Resource withDescription(String description); /** * Gets the launch time of the resource. * * @return the launch time of the resource */ Date getLaunchTime(); /** * Sets the launch time of the resource. * * @param launchTime the launch time of the resource */ void setLaunchTime(Date launchTime); /** * Sets the resource launch time and returns the resource. * * @param launchTime the launch time of the resource * @return the resource object */ Resource withLaunchTime(Date launchTime); /** * Gets the time that when the resource is marked as a cleanup candidate. * * @return the time that when the resource is marked as a cleanup candidate */ Date getMarkTime(); /** * Sets the time that when the resource is marked as a cleanup candidate. * * @param markTime the time that when the resource is marked as a cleanup candidate */ void setMarkTime(Date markTime); /** * Sets the resource mark time and returns the resource. * * @param markTime the time that when the resource is marked as a cleanup candidate * @return the resource object */ Resource withMarkTime(Date markTime); /** * Gets the the time that when the resource is expected to be terminated. * * @return the time that when the resource is expected to be terminated */ Date getExpectedTerminationTime(); /** * Sets the time that when the resource is expected to be terminated. * * @param expectedTerminationTime the time that when the resource is expected to be terminated */ void setExpectedTerminationTime(Date expectedTerminationTime); /** * Sets the time that when the resource is expected to be terminated and returns the resource. * * @param expectedTerminationTime the time that when the resource is expected to be terminated * @return the resource object */ Resource withExpectedTerminationTime(Date expectedTerminationTime); /** * Gets the time that when the resource is actually terminated. * * @return the time that when the resource is actually terminated */ Date getActualTerminationTime(); /** * Sets the time that when the resource is actually terminated. * * @param actualTerminationTime the time that when the resource is actually terminated */ void setActualTerminationTime(Date actualTerminationTime); /** * Sets the resource actual termination time and returns the resource. * * @param actualTerminationTime the time that when the resource is actually terminated * @return the resource object */ Resource withActualTerminationTime(Date actualTerminationTime); /** * Gets the time that when the owner is notified about the cleanup of the resource. * * @return the time that when the owner is notified about the cleanup of the resource */ Date getNotificationTime(); /** * Sets the time that when the owner is notified about the cleanup of the resource. * * @param notificationTime the time that when the owner is notified about the cleanup of the resource */ void setNotificationTime(Date notificationTime); /** * Sets the time that when the owner is notified about the cleanup of the resource and returns the resource. * * @param notificationTime the time that when the owner is notified about the cleanup of the resource * @return the resource object */ Resource withNnotificationTime(Date notificationTime); /** * Gets the resource state. * * @return the resource state enum */ CleanupState getState(); /** * Sets the resource state. * * @param state the resource state */ void setState(CleanupState state); /** * Sets the resource state and returns the resource. * * @param state resource state enum * @return the resource object */ Resource withState(CleanupState state); /** * Gets the termination reason of the resource. * * @return the termination reason of the resource */ String getTerminationReason(); /** * Sets the termination reason of the resource. * * @param terminationReason the termination reason of the resource */ void setTerminationReason(String terminationReason); /** * Sets the resource termination reason and returns the resource. * * @param terminationReason the termination reason of the resource * @return the resource object */ Resource withTerminationReason(String terminationReason); /** * Gets the boolean to indicate whether or not the resource is opted out of Janitor monkey * so it will not be cleaned. * @return true if the resource is opted out of Janitor monkey, otherwise false */ boolean isOptOutOfJanitor(); /** * Sets the flag to indicate whether or not the resource is opted out of Janitor monkey * so it will not be cleaned. * @param optOutOfJanitor true if the resource is opted out of Janitor monkey, otherwise false */ void setOptOutOfJanitor(boolean optOutOfJanitor); /** * Sets the flag to indicate whether or not the resource is opted out of Janitor monkey * so it will not be cleaned and returns the resource object. * @param optOutOfJanitor true if the resource is opted out of Janitor monkey, otherwise false * @return the resource object */ Resource withOptOutOfJanitor(boolean optOutOfJanitor); /** * Gets a map from fields of resources to corresponding values. Values are represented * as Strings so they can be displayed or stored in databases like SimpleDB. * @return a map from field name to field value */ Map getFieldToValueMap(); /** Adds or sets an additional field with the specified name and value to the resource. * * @param fieldName the field name * @param fieldValue the field value * @return the resource itself for chaining */ Resource setAdditionalField(String fieldName, String fieldValue); /** Gets the value of an additional field with the specified name of the resource. * * @param fieldName the field name * @return the field value */ String getAdditionalField(String fieldName); /** * Gets all additional field names in the resource. * @return a collection of names of all additional fields */ Collection getAdditionalFieldNames(); /** * Adds a tag with the specified key and value to the resource. * @param key the key of the tag * @param value the value of the tag */ void setTag(String key, String value); /** * Gets the tag value for a specific key of the resource. * @param key the key of the tag * @return the value of the tag */ String getTag(String key); /** * Gets all the keys of tags. * @return collection of keys of all tags */ Collection getAllTagKeys(); /** Clone a resource with the exact field values of the current object. * * @return the clone of the resource */ Resource cloneResource(); } ================================================ FILE: src/main/java/com/netflix/simianarmy/ResourceType.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; /** * Marker interface for all resource type enumerations. */ public interface ResourceType extends NamedType { } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/AWSEmailNotifier.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.amazonaws.services.simpleemail.model.Body; import com.amazonaws.services.simpleemail.model.Content; import com.amazonaws.services.simpleemail.model.Destination; import com.amazonaws.services.simpleemail.model.Message; import com.amazonaws.services.simpleemail.model.SendEmailRequest; import com.amazonaws.services.simpleemail.model.SendEmailResult; import com.netflix.simianarmy.MonkeyEmailNotifier; /** * The class implements the monkey email notifier using AWS simple email service * for sending email. */ public abstract class AWSEmailNotifier implements MonkeyEmailNotifier { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(AWSEmailNotifier.class); private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+\\.#]+(.[_A-Za-z0-9-#]+)*@" + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; private final Pattern emailPattern; private final AmazonSimpleEmailServiceClient sesClient; /** * The constructor. */ public AWSEmailNotifier(AmazonSimpleEmailServiceClient sesClient) { super(); this.sesClient = sesClient; this.emailPattern = Pattern.compile(EMAIL_PATTERN); } @Override public void sendEmail(String to, String subject, String body) { if (!isValidEmail(to)) { LOGGER.error(String.format("The destination email address %s is not valid, no email is sent.", to)); return; } if (sesClient == null) { String msg = "The email client is not set."; LOGGER.error(msg); throw new RuntimeException(msg); } Destination destination = new Destination().withToAddresses(to) .withCcAddresses(getCcAddresses(to)); Content subjectContent = new Content(subject); Content bodyContent = new Content(); Body msgBody = new Body(bodyContent); msgBody.setHtml(new Content(body)); Message msg = new Message(subjectContent, msgBody); String sourceAddress = getSourceAddress(to); SendEmailRequest request = new SendEmailRequest(sourceAddress, destination, msg); request.setReturnPath(sourceAddress); LOGGER.debug(String.format("Sending email with subject '%s' to %s", subject, to)); SendEmailResult result = null; try { result = sesClient.sendEmail(request); } catch (Exception e) { throw new RuntimeException(String.format("Failed to send email to %s", to), e); } LOGGER.info(String.format("Email to %s, result id is %s, subject is %s", to, result.getMessageId(), subject)); } @Override public boolean isValidEmail(String email) { if (email == null) { return false; } if (!emailPattern.matcher(email).matches()) { LOGGER.error(String.format("Invalid email address: %s", email)); return false; } if (email.equals("foo@bar.com")) { LOGGER.error(String.format("Email address not changed from default; treating as invalid: %s", email)); return false; } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/AWSResource.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws; import java.util.*; import org.apache.commons.lang.Validate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import com.netflix.simianarmy.NamedType; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; /** * The class represents general AWS resources that are managed by janitor monkey. */ public class AWSResource implements Resource { private String id; private ResourceType resourceType; private String region; private String ownerEmail; private String description; private String terminationReason; private CleanupState state; private Date expectedTerminationTime; private Date actualTerminationTime; private Date notificationTime; private Date launchTime; private Date markTime; private boolean optOutOfJanitor; private String awsResourceState; /** The field name for resourceId. **/ public static final String FIELD_RESOURCE_ID = "resourceId"; /** The field name for resourceType. **/ public static final String FIELD_RESOURCE_TYPE = "resourceType"; /** The field name for region. **/ public static final String FIELD_REGION = "region"; /** The field name for owner email. **/ public static final String FIELD_OWNER_EMAIL = "ownerEmail"; /** The field name for description. **/ public static final String FIELD_DESCRIPTION = "description"; /** The field name for state. **/ public static final String FIELD_STATE = "state"; /** The field name for terminationReason. **/ public static final String FIELD_TERMINATION_REASON = "terminationReason"; /** The field name for expectedTerminationTime. **/ public static final String FIELD_EXPECTED_TERMINATION_TIME = "expectedTerminationTime"; /** The field name for actualTerminationTime. **/ public static final String FIELD_ACTUAL_TERMINATION_TIME = "actualTerminationTime"; /** The field name for notificationTime. **/ public static final String FIELD_NOTIFICATION_TIME = "notificationTime"; /** The field name for launchTime. **/ public static final String FIELD_LAUNCH_TIME = "launchTime"; /** The field name for markTime. **/ public static final String FIELD_MARK_TIME = "markTime"; /** The field name for isOptOutOfJanitor. **/ public static final String FIELD_OPT_OUT_OF_JANITOR = "optOutOfJanitor"; /** The field name for awsResourceState. **/ public static final String FIELD_AWS_RESOURCE_STATE = "awsResourceState"; /** The date format used to print or parse a Date value. **/ public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss"); /** The map from name to value for additional fields used by the resource. **/ private final Map additionalFields = new HashMap(); /** The map from AWS tag key to value for the resource. **/ private final Map tags = new HashMap(); /** {@inheritDoc} */ @Override public Map getFieldToValueMap() { Map fieldToValue = new HashMap(); putToMapIfNotNull(fieldToValue, FIELD_RESOURCE_ID, getId()); putToMapIfNotNull(fieldToValue, FIELD_RESOURCE_TYPE, getResourceType()); putToMapIfNotNull(fieldToValue, FIELD_REGION, getRegion()); putToMapIfNotNull(fieldToValue, FIELD_OWNER_EMAIL, getOwnerEmail()); putToMapIfNotNull(fieldToValue, FIELD_DESCRIPTION, getDescription()); putToMapIfNotNull(fieldToValue, FIELD_STATE, getState()); putToMapIfNotNull(fieldToValue, FIELD_TERMINATION_REASON, getTerminationReason()); putToMapIfNotNull(fieldToValue, FIELD_EXPECTED_TERMINATION_TIME, printDate(getExpectedTerminationTime())); putToMapIfNotNull(fieldToValue, FIELD_ACTUAL_TERMINATION_TIME, printDate(getActualTerminationTime())); putToMapIfNotNull(fieldToValue, FIELD_NOTIFICATION_TIME, printDate(getNotificationTime())); putToMapIfNotNull(fieldToValue, FIELD_LAUNCH_TIME, printDate(getLaunchTime())); putToMapIfNotNull(fieldToValue, FIELD_MARK_TIME, printDate(getMarkTime())); putToMapIfNotNull(fieldToValue, FIELD_AWS_RESOURCE_STATE, getAWSResourceState()); // Additional fields are serialized while tags are not. So if any tags need to be // serialized as well, put them to additional fields. fieldToValue.put(FIELD_OPT_OUT_OF_JANITOR, String.valueOf(isOptOutOfJanitor())); fieldToValue.putAll(additionalFields); return fieldToValue; } /** * Parse a map from field name to value to a resource. * @param fieldToValue the map from field name to value * @return the resource that is de-serialized from the map */ public static AWSResource parseFieldtoValueMap(Map fieldToValue) { AWSResource resource = new AWSResource(); for (Map.Entry field : fieldToValue.entrySet()) { String name = field.getKey(); String value = field.getValue(); if (name.equals(FIELD_RESOURCE_ID)) { resource.setId(value); } else if (name.equals(FIELD_RESOURCE_TYPE)) { resource.setResourceType(AWSResourceType.valueOf(value)); } else if (name.equals(FIELD_REGION)) { resource.setRegion(value); } else if (name.equals(FIELD_OWNER_EMAIL)) { resource.setOwnerEmail(value); } else if (name.equals(FIELD_DESCRIPTION)) { resource.setDescription(value); } else if (name.equals(FIELD_STATE)) { resource.setState(CleanupState.valueOf(value)); } else if (name.equals(FIELD_TERMINATION_REASON)) { resource.setTerminationReason(value); } else if (name.equals(FIELD_EXPECTED_TERMINATION_TIME)) { resource.setExpectedTerminationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis())); } else if (name.equals(FIELD_ACTUAL_TERMINATION_TIME)) { resource.setActualTerminationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis())); } else if (name.equals(FIELD_NOTIFICATION_TIME)) { resource.setNotificationTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis())); } else if (name.equals(FIELD_LAUNCH_TIME)) { resource.setLaunchTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis())); } else if (name.equals(FIELD_MARK_TIME)) { resource.setMarkTime(new Date(DATE_FORMATTER.parseDateTime(value).getMillis())); } else if (name.equals(FIELD_AWS_RESOURCE_STATE)) { resource.setAWSResourceState(value); } else if (name.equals(FIELD_OPT_OUT_OF_JANITOR)) { resource.setOptOutOfJanitor("true".equals(value)); } else { // put all other fields into additional fields resource.setAdditionalField(name, value); } } return resource; } public String getAWSResourceState() { return awsResourceState; } public void setAWSResourceState(String awsState) { this.awsResourceState = awsState; } /** {@inheritDoc} */ @Override public String getId() { return id; } /** {@inheritDoc} */ @Override public void setId(String id) { this.id = id; } /** {@inheritDoc} */ @Override public Resource withId(String resourceId) { setId(resourceId); return this; } /** {@inheritDoc} */ @Override public ResourceType getResourceType() { return resourceType; } /** {@inheritDoc} */ @Override public void setResourceType(ResourceType resourceType) { this.resourceType = resourceType; } /** {@inheritDoc} */ @Override public Resource withResourceType(ResourceType type) { setResourceType(type); return this; } /** {@inheritDoc} */ @Override public String getRegion() { return region; } /** {@inheritDoc} */ @Override public void setRegion(String region) { this.region = region; } /** {@inheritDoc} */ @Override public Resource withRegion(String resourceRegion) { setRegion(resourceRegion); return this; } /** {@inheritDoc} */ @Override public String getOwnerEmail() { return ownerEmail; } /** {@inheritDoc} */ @Override public void setOwnerEmail(String ownerEmail) { this.ownerEmail = ownerEmail; } /** {@inheritDoc} */ @Override public Resource withOwnerEmail(String resourceOwner) { setOwnerEmail(resourceOwner); return this; } /** {@inheritDoc} */ @Override public String getDescription() { return description; } /** {@inheritDoc} */ @Override public void setDescription(String description) { this.description = description; } /** {@inheritDoc} */ @Override public Resource withDescription(String resourceDescription) { setDescription(resourceDescription); return this; } /** {@inheritDoc} */ @Override public Date getLaunchTime() { return getCopyOfDate(launchTime); } /** {@inheritDoc} */ @Override public void setLaunchTime(Date launchTime) { this.launchTime = getCopyOfDate(launchTime); } /** {@inheritDoc} */ @Override public Resource withLaunchTime(Date resourceLaunchTime) { setLaunchTime(resourceLaunchTime); return this; } /** {@inheritDoc} */ @Override public Date getMarkTime() { return getCopyOfDate(markTime); } /** {@inheritDoc} */ @Override public void setMarkTime(Date markTime) { this.markTime = getCopyOfDate(markTime); } /** {@inheritDoc} */ @Override public Resource withMarkTime(Date resourceMarkTime) { setMarkTime(resourceMarkTime); return this; } /** {@inheritDoc} */ @Override public Date getExpectedTerminationTime() { return getCopyOfDate(expectedTerminationTime); } /** {@inheritDoc} */ @Override public void setExpectedTerminationTime(Date expectedTerminationTime) { this.expectedTerminationTime = getCopyOfDate(expectedTerminationTime); } /** {@inheritDoc} */ @Override public Resource withExpectedTerminationTime(Date resourceExpectedTerminationTime) { setExpectedTerminationTime(resourceExpectedTerminationTime); return this; } /** {@inheritDoc} */ @Override public Date getActualTerminationTime() { return getCopyOfDate(actualTerminationTime); } /** {@inheritDoc} */ @Override public void setActualTerminationTime(Date actualTerminationTime) { this.actualTerminationTime = getCopyOfDate(actualTerminationTime); } /** {@inheritDoc} */ @Override public Resource withActualTerminationTime(Date resourceActualTerminationTime) { setActualTerminationTime(resourceActualTerminationTime); return this; } /** {@inheritDoc} */ @Override public Date getNotificationTime() { return getCopyOfDate(notificationTime); } /** {@inheritDoc} */ @Override public void setNotificationTime(Date notificationTime) { this.notificationTime = getCopyOfDate(notificationTime); } /** {@inheritDoc} */ @Override public Resource withNnotificationTime(Date resourceNotificationTime) { setNotificationTime(resourceNotificationTime); return this; } /** {@inheritDoc} */ @Override public CleanupState getState() { return state; } /** {@inheritDoc} */ @Override public void setState(CleanupState state) { this.state = state; } /** {@inheritDoc} */ @Override public Resource withState(CleanupState resourceState) { setState(resourceState); return this; } /** {@inheritDoc} */ @Override public String getTerminationReason() { return terminationReason; } /** {@inheritDoc} */ @Override public void setTerminationReason(String terminationReason) { this.terminationReason = terminationReason; } /** {@inheritDoc} */ @Override public Resource withTerminationReason(String resourceTerminationReason) { setTerminationReason(resourceTerminationReason); return this; } /** {@inheritDoc} */ @Override public boolean isOptOutOfJanitor() { return optOutOfJanitor; } /** {@inheritDoc} */ @Override public void setOptOutOfJanitor(boolean optOutOfJanitor) { this.optOutOfJanitor = optOutOfJanitor; } /** {@inheritDoc} */ @Override public Resource withOptOutOfJanitor(boolean optOut) { setOptOutOfJanitor(optOut); return this; } private static Date getCopyOfDate(Date date) { if (date == null) { return null; } return new Date(date.getTime()); } private static void putToMapIfNotNull(Map map, String key, String value) { Validate.notNull(map); Validate.notNull(key); if (value != null) { map.put(key, value); } } private static void putToMapIfNotNull(Map map, String key, Enum value) { Validate.notNull(map); Validate.notNull(key); if (value != null) { map.put(key, value.name()); } } private static void putToMapIfNotNull(Map map, String key, NamedType value) { Validate.notNull(map); Validate.notNull(key); if (value != null) { map.put(key, value.name()); } } private static String printDate(Date date) { if (date == null) { return null; } return DATE_FORMATTER.print(date.getTime()); } @Override public Resource setAdditionalField(String fieldName, String fieldValue) { Validate.notNull(fieldName); Validate.notNull(fieldValue); putToMapIfNotNull(additionalFields, fieldName, fieldValue); return this; } @Override public String getAdditionalField(String fieldName) { return additionalFields.get(fieldName); } @Override public Collection getAdditionalFieldNames() { return additionalFields.keySet(); } @Override public Resource cloneResource() { Resource clone = new AWSResource() .withActualTerminationTime(getActualTerminationTime()) .withDescription(getDescription()) .withExpectedTerminationTime(getExpectedTerminationTime()) .withId(getId()) .withLaunchTime(getLaunchTime()) .withMarkTime(getMarkTime()) .withNnotificationTime(getNotificationTime()) .withOwnerEmail(getOwnerEmail()) .withRegion(getRegion()) .withResourceType(getResourceType()) .withState(getState()) .withTerminationReason(getTerminationReason()) .withOptOutOfJanitor(isOptOutOfJanitor()); ((AWSResource) clone).setAWSResourceState(awsResourceState); ((AWSResource) clone).additionalFields.putAll(additionalFields); for (String key : this.getAllTagKeys()) { clone.setTag(key, this.getTag(key)); } return clone; } /** {@inheritDoc} */ @Override public void setTag(String key, String value) { tags.put(key, value); } /** {@inheritDoc} */ @Override public String getTag(String key) { return tags.get(key); } /** {@inheritDoc} */ @Override public Collection getAllTagKeys() { return tags.keySet(); } @Override public String toString() { return "AWSResource{" + "id='" + id + '\'' + ", resourceType=" + resourceType + ", region='" + region + '\'' + ", ownerEmail='" + ownerEmail + '\'' + ", description='" + description + '\'' + ", terminationReason='" + terminationReason + '\'' + ", state=" + state + ", expectedTerminationTime=" + expectedTerminationTime + ", actualTerminationTime=" + actualTerminationTime + ", notificationTime=" + notificationTime + ", launchTime=" + launchTime + ", markTime=" + markTime + ", optOutOfJanitor=" + optOutOfJanitor + ", awsResourceState='" + awsResourceState + '\'' + ", additionalFields=" + additionalFields + ", tags=" + tags + '}'; } @Override public boolean equals(Object o) { // consider two resources to be equivalent if id, resourceType and region match if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AWSResource that = (AWSResource) o; return Objects.equals(id, that.id) && Objects.equals(resourceType, that.resourceType) && Objects.equals(region, that.region); } @Override public int hashCode() { // consider two resources to be equivalent if id, resourceType and region match return Objects.hash(id, resourceType, region); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/AWSResourceType.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws; import com.netflix.simianarmy.ResourceType; /** * The enum of resource types of AWS. */ public enum AWSResourceType implements ResourceType { /** AWS instance. */ INSTANCE, /** AWS EBS volume. */ EBS_VOLUME, /** AWS EBS snapshot. */ EBS_SNAPSHOT, /** AWS auto scaling group. */ ASG, /** AWS launch configuration. */ LAUNCH_CONFIG, /** AWS S3 bucket. */ S3_BUCKET, /** AWS security group. */ SECURITY_GROUP, /** AWS Amazon Machine Image. **/ IMAGE, /** AWS Elastic Load Balancer. **/ ELB } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/RDSRecorder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws; import com.amazonaws.AmazonClientException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.MonkeyRecorder; import com.netflix.simianarmy.MonkeyType; import com.netflix.simianarmy.basic.BasicRecorderEvent; import com.zaxxer.hikari.HikariDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; /** * The Class RDSRecorder. Records events to and fetched events from a RDS table (default SIMIAN_ARMY) */ @SuppressWarnings("serial") public class RDSRecorder implements MonkeyRecorder { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(RDSRecorder.class); private final String region; /** The table. */ private final String table; /** the jdbcTemplate */ JdbcTemplate jdbcTemplate = null; public static final String FIELD_ID = "eventId"; public static final String FIELD_EVENT_TIME = "eventTime"; public static final String FIELD_MONKEY_TYPE = "monkeyType"; public static final String FIELD_EVENT_TYPE = "eventType"; public static final String FIELD_REGION = "region"; public static final String FIELD_DATA_JSON = "dataJson"; /** * Instantiates a new RDS recorder. * */ public RDSRecorder(String dbDriver, String dbUser, String dbPass, String dbUrl, String dbTable, String region) { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName(dbDriver); dataSource.setJdbcUrl(dbUrl); dataSource.setUsername(dbUser); dataSource.setPassword(dbPass); dataSource.setMaximumPoolSize(2); this.jdbcTemplate = new JdbcTemplate(dataSource); this.table = dbTable; this.region = region; } /** * Instantiates a new RDS recorder. This constructor is intended * for unit testing. * */ public RDSRecorder(JdbcTemplate jdbcTemplate, String table, String region) { this.jdbcTemplate = jdbcTemplate; this.table = table; this.region = region; } public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } /** {@inheritDoc} */ @Override public Event newEvent(MonkeyType monkeyType, EventType eventType, String reg, String id) { return new BasicRecorderEvent(monkeyType, eventType, reg, id); } /** {@inheritDoc} */ @Override public void recordEvent(Event evt) { String evtTime = String.valueOf(evt.eventTime().getTime()); String name = String.format("%s-%s-%s-%s", evt.monkeyType().name(), evt.id(), region, evtTime); String json; try { json = new ObjectMapper().writeValueAsString(evt.fields()); } catch (JsonProcessingException e) { LOGGER.error("ERROR generating JSON when saving resource " + name, e); return; } LOGGER.debug(String.format("Saving event %s to RDS table %s", name, table)); StringBuilder sb = new StringBuilder(); sb.append("insert into ").append(table); sb.append(" ("); sb.append(FIELD_ID).append(","); sb.append(FIELD_EVENT_TIME).append(","); sb.append(FIELD_MONKEY_TYPE).append(","); sb.append(FIELD_EVENT_TYPE).append(","); sb.append(FIELD_REGION).append(","); sb.append(FIELD_DATA_JSON).append(") values (?,?,?,?,?,?)"); LOGGER.debug(String.format("Insert statement is '%s'", sb)); int updated = this.jdbcTemplate.update(sb.toString(), evt.id(), evt.eventTime().getTime(), SimpleDBRecorder.enumToValue(evt.monkeyType()), SimpleDBRecorder.enumToValue(evt.eventType()), evt.region(), json); LOGGER.debug(String.format("%d rows inserted", updated)); } /** {@inheritDoc} */ @Override public List findEvents(Map query, Date after) { return findEvents(null, null, query, after); } /** {@inheritDoc} */ @Override public List findEvents(MonkeyType monkeyType, Map query, Date after) { return findEvents(monkeyType, null, query, after); } /** {@inheritDoc} */ @Override public List findEvents(MonkeyType monkeyType, EventType eventType, Map query, Date after) { ArrayList args = new ArrayList<>(); StringBuilder sqlquery = new StringBuilder( String.format("select * from %s where region = ?", table)); args.add(region); if (monkeyType != null) { sqlquery.append(String.format(" and %s = ?", FIELD_MONKEY_TYPE)); args.add(SimpleDBRecorder.enumToValue(monkeyType)); } if (eventType != null) { sqlquery.append(String.format(" and %s = ?", FIELD_EVENT_TYPE)); args.add(SimpleDBRecorder.enumToValue(eventType)); } for (Map.Entry pair : query.entrySet()) { sqlquery.append(String.format(" and %s like ?", FIELD_DATA_JSON)); args.add((String.format("%s: \"%s\"", pair.getKey(), pair.getValue()))); } sqlquery.append(String.format(" and %s > ? order by %s desc", FIELD_EVENT_TIME, FIELD_EVENT_TIME)); args.add(new Long(after.getTime())); LOGGER.debug(String.format("Query is '%s'", sqlquery)); List events = jdbcTemplate.query(sqlquery.toString(), args.toArray(), new RowMapper() { public Event mapRow(ResultSet rs, int rowNum) throws SQLException { return mapEvent(rs); } }); return events; } private Event mapEvent(ResultSet rs) throws SQLException { String json = rs.getString("dataJson"); ObjectMapper mapper = new ObjectMapper(); Event event = null; try { String id = rs.getString(FIELD_ID); MonkeyType monkeyType = SimpleDBRecorder.valueToEnum(MonkeyType.class, rs.getString(FIELD_MONKEY_TYPE)); EventType eventType = SimpleDBRecorder.valueToEnum(EventType.class, rs.getString(FIELD_EVENT_TYPE)); String region = rs.getString(FIELD_REGION); long time = rs.getLong(FIELD_EVENT_TIME); event = new BasicRecorderEvent(monkeyType, eventType, region, id, time); TypeReference> typeRef = new TypeReference>() {}; Map map = mapper.readValue(json, typeRef); for(String key : map.keySet()) { event.addField(key, map.get(key)); } }catch(IOException ie) { LOGGER.error("Error parsing resource from json", ie); } return event; } /** * Creates the RDS table, if it does not already exist. */ public void init() { try { if (this.region == null || this.region.equals("region-null")) { // This is a mock with an invalid region; avoid a slow timeout LOGGER.debug("Region=null; skipping RDS table creation"); return; } LOGGER.info("Creating RDS table: {}", table); String sql = String.format("create table if not exists %s (" + " %s varchar(255)," + " %s BIGINT," + " %s varchar(255)," + " %s varchar(255)," + " %s varchar(255)," + " %s varchar(4096) )", table, FIELD_ID, FIELD_EVENT_TIME, FIELD_MONKEY_TYPE, FIELD_EVENT_TYPE, FIELD_REGION, FIELD_DATA_JSON); LOGGER.debug("Create SQL is: '{}'", sql); jdbcTemplate.execute(sql); } catch (AmazonClientException e) { LOGGER.warn("Error while trying to auto-create RDS table", e); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/STSAssumeRoleSessionCredentialsProvider.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws; import java.util.Date; import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSSessionCredentials; import com.amazonaws.auth.BasicSessionCredentials; import com.amazonaws.services.securitytoken.AWSSecurityTokenService; import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient; import com.amazonaws.services.securitytoken.model.AssumeRoleRequest; import com.amazonaws.services.securitytoken.model.AssumeRoleResult; import com.amazonaws.services.securitytoken.model.Credentials; /** * AWSCredentialsProvider implementation that uses the AWS Security Token * Service to assume a Role and create temporary, short-lived sessions to use * for authentication. */ public class STSAssumeRoleSessionCredentialsProvider implements AWSCredentialsProvider { /** Default duration for started sessions. */ public static final int DEFAULT_DURATION_SECONDS = 900; /** Time before expiry within which credentials will be renewed. */ private static final int EXPIRY_TIME_MILLIS = 60 * 1000; /** The client for starting STS sessions. */ private final AWSSecurityTokenService securityTokenService; /** The current session credentials. */ private AWSSessionCredentials sessionCredentials; /** The expiration time for the current session credentials. */ private Date sessionCredentialsExpiration; /** The arn of the role to be assumed. */ private String roleArn; /** * Constructs a new STSAssumeRoleSessionCredentialsProvider, which makes a * request to the AWS Security Token Service (STS), uses the provided * {@link #roleArn} to assume a role and then request short lived session * credentials, which will then be returned by this class's * {@link #getCredentials()} method. * @param roleArn * The AWS ARN of the Role to be assumed. */ public STSAssumeRoleSessionCredentialsProvider(String roleArn) { this.roleArn = roleArn; securityTokenService = new AWSSecurityTokenServiceClient(); } /** * Constructs a new STSAssumeRoleSessionCredentialsProvider, which makes a * request to the AWS Security Token Service (STS), uses the provided * {@link #roleArn} to assume a role and then request short lived session * credentials, which will then be returned by this class's * {@link #getCredentials()} method. * @param roleArn * The AWS ARN of the Role to be assumed. * @param clientConfiguration * The AWS ClientConfiguration to use when making AWS API requests. */ public STSAssumeRoleSessionCredentialsProvider(String roleArn, ClientConfiguration clientConfiguration) { this.roleArn = roleArn; securityTokenService = new AWSSecurityTokenServiceClient(clientConfiguration); } /** * Constructs a new STSAssumeRoleSessionCredentialsProvider, which will use * the specified long lived AWS credentials to make a request to the AWS * Security Token Service (STS), uses the provided {@link #roleArn} to * assume a role and then request short lived session credentials, which * will then be returned by this class's {@link #getCredentials()} method. * @param longLivedCredentials * The main AWS credentials for a user's account. * @param roleArn * The AWS ARN of the Role to be assumed. */ public STSAssumeRoleSessionCredentialsProvider(AWSCredentials longLivedCredentials, String roleArn) { this(longLivedCredentials, roleArn, new ClientConfiguration()); } /** * Constructs a new STSAssumeRoleSessionCredentialsProvider, which will use * the specified long lived AWS credentials to make a request to the AWS * Security Token Service (STS), uses the provided {@link #roleArn} to * assume a role and then request short lived session credentials, which * will then be returned by this class's {@link #getCredentials()} method. * @param longLivedCredentials * The main AWS credentials for a user's account. * @param roleArn * The AWS ARN of the Role to be assumed. * @param clientConfiguration * Client configuration connection parameters. */ public STSAssumeRoleSessionCredentialsProvider(AWSCredentials longLivedCredentials, String roleArn, ClientConfiguration clientConfiguration) { this.roleArn = roleArn; securityTokenService = new AWSSecurityTokenServiceClient(longLivedCredentials, clientConfiguration); } /** * Constructs a new STSAssumeRoleSessionCredentialsProvider, which will use * the specified credentials provider (which vends long lived AWS * credentials) to make a request to the AWS Security Token Service (STS), * usess the provided {@link #roleArn} to assume a role and then request * short lived session credentials, which will then be returned by this * class's {@link #getCredentials()} method. * @param longLivedCredentialsProvider * Credentials provider for the main AWS credentials for a user's * account. * @param roleArn * The AWS ARN of the Role to be assumed. */ public STSAssumeRoleSessionCredentialsProvider(AWSCredentialsProvider longLivedCredentialsProvider, String roleArn) { this.roleArn = roleArn; securityTokenService = new AWSSecurityTokenServiceClient(longLivedCredentialsProvider); } /** * Constructs a new STSAssumeRoleSessionCredentialsProvider, which will use * the specified credentials provider (which vends long lived AWS * credentials) to make a request to the AWS Security Token Service (STS), * uses the provided {@link #roleArn} to assume a role and then request * short lived session credentials, which will then be returned by this * class's {@link #getCredentials()} method. * @param longLivedCredentialsProvider * Credentials provider for the main AWS credentials for a user's * account. * @param roleArn * The AWS ARN of the Role to be assumed. * @param clientConfiguration * Client configuration connection parameters. */ public STSAssumeRoleSessionCredentialsProvider(AWSCredentialsProvider longLivedCredentialsProvider, String roleArn, ClientConfiguration clientConfiguration) { this.roleArn = roleArn; securityTokenService = new AWSSecurityTokenServiceClient(longLivedCredentialsProvider, clientConfiguration); } @Override public AWSCredentials getCredentials() { if (needsNewSession()) { startSession(); } return sessionCredentials; } @Override public void refresh() { startSession(); } /** * Starts a new session by sending a request to the AWS Security Token * Service (STS) to assume a Role using the long lived AWS credentials. This * class then vends the short lived session credentials for the assumed Role * sent back from STS. */ private void startSession() { AssumeRoleResult assumeRoleResult = securityTokenService.assumeRole(new AssumeRoleRequest() .withRoleArn(roleArn).withDurationSeconds(DEFAULT_DURATION_SECONDS).withRoleSessionName("SimianArmy")); Credentials stsCredentials = assumeRoleResult.getCredentials(); sessionCredentials = new BasicSessionCredentials(stsCredentials.getAccessKeyId(), stsCredentials.getSecretAccessKey(), stsCredentials.getSessionToken()); sessionCredentialsExpiration = stsCredentials.getExpiration(); } /** * Returns true if a new STS session needs to be started. A new STS session * is needed when no session has been started yet, or if the last session is * within {@link #EXPIRY_TIME_MILLIS} seconds of expiring. * @return True if a new STS session needs to be started. */ private boolean needsNewSession() { if (sessionCredentials == null) { return true; } long timeRemaining = sessionCredentialsExpiration.getTime() - System.currentTimeMillis(); return timeRemaining < EXPIRY_TIME_MILLIS; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/SimpleDBRecorder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.AmazonClientException; import com.amazonaws.services.simpledb.AmazonSimpleDB; import com.amazonaws.services.simpledb.model.Attribute; import com.amazonaws.services.simpledb.model.CreateDomainRequest; import com.amazonaws.services.simpledb.model.Item; import com.amazonaws.services.simpledb.model.ListDomainsResult; import com.amazonaws.services.simpledb.model.PutAttributesRequest; import com.amazonaws.services.simpledb.model.ReplaceableAttribute; import com.amazonaws.services.simpledb.model.SelectRequest; import com.amazonaws.services.simpledb.model.SelectResult; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.MonkeyRecorder; import com.netflix.simianarmy.MonkeyType; import com.netflix.simianarmy.NamedType; import com.netflix.simianarmy.basic.BasicRecorderEvent; import com.netflix.simianarmy.client.aws.AWSClient; /** * The Class SimpleDBRecorder. Records events to and fetched events from a Amazon SimpleDB table (default SIMIAN_ARMY) */ @SuppressWarnings("serial") public class SimpleDBRecorder implements MonkeyRecorder { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDBRecorder.class); private final AmazonSimpleDB simpleDBClient; private final String region; /** The domain. */ private final String domain; /** * The Enum Keys. */ private enum Keys { /** The event id. */ id, /** The event time. */ eventTime, /** The region. */ region, /** The record type. */ recordType, /** The monkey type. */ monkeyType, /** The event type. */ eventType; /** The Constant KEYSET. */ public static final Set KEYSET = Collections.unmodifiableSet(new HashSet() { { for (Keys k : Keys.values()) { add(k.toString()); } } }); }; /** * Instantiates a new simple db recorder. * * @param awsClient * the AWS client * @param domain * the domain */ public SimpleDBRecorder(AWSClient awsClient, String domain) { Validate.notNull(awsClient); Validate.notNull(domain); this.simpleDBClient = awsClient.sdbClient(); this.region = awsClient.region(); this.domain = domain; } /** * simple client. abstracted to aid testing * * @return the amazon simple db */ protected AmazonSimpleDB sdbClient() { return simpleDBClient; } /** * Enum to value. Converts an enum to "name|type" string * * @param e * the e * @return the string */ public static String enumToValue(NamedType e) { return String.format("%s|%s", e.name(), e.getClass().getName()); } /** * Value to enum. Converts a "name|type" string back to an enum. * * @param value * the value * @return the enum */ public static T valueToEnum( Class type, String value) { // parts = [enum value, enum class type] String[] parts = value.split("\\|", 2); if (parts.length < 2) { throw new RuntimeException("value " + value + " does not appear to be an internal enum format"); } Class enumClass; try { enumClass = Class.forName(parts[1]); } catch (ClassNotFoundException e) { throw new RuntimeException("class for enum value " + value + " not found"); } if (!enumClass.isEnum()) { throw new RuntimeException("value " + value + " does not appear to be of an enum type"); } if (!type.isAssignableFrom(enumClass)) { throw new RuntimeException("value " + value + " cannot be assigned to a variable of this type: " + type.getCanonicalName()); } @SuppressWarnings("rawtypes") Class enumType = enumClass.asSubclass(Enum.class); @SuppressWarnings("unchecked") T enumValue = (T) Enum.valueOf(enumType, parts[0]); return enumValue; } /** {@inheritDoc} */ @Override public Event newEvent(MonkeyType monkeyType, EventType eventType, String reg, String id) { return new BasicRecorderEvent(monkeyType, eventType, reg, id); } /** {@inheritDoc} */ @Override public void recordEvent(Event evt) { String evtTime = String.valueOf(evt.eventTime().getTime()); List attrs = new LinkedList(); attrs.add(new ReplaceableAttribute(Keys.id.name(), evt.id(), true)); attrs.add(new ReplaceableAttribute(Keys.eventTime.name(), evtTime, true)); attrs.add(new ReplaceableAttribute(Keys.region.name(), evt.region(), true)); attrs.add(new ReplaceableAttribute(Keys.recordType.name(), "MonkeyEvent", true)); attrs.add(new ReplaceableAttribute(Keys.monkeyType.name(), enumToValue(evt.monkeyType()), true)); attrs.add(new ReplaceableAttribute(Keys.eventType.name(), enumToValue(evt.eventType()), true)); for (Map.Entry pair : evt.fields().entrySet()) { if (pair.getValue() == null || pair.getValue().equals("") || Keys.KEYSET.contains(pair.getKey())) { continue; } attrs.add(new ReplaceableAttribute(pair.getKey(), pair.getValue(), true)); } // Let pk contain the timestamp so that the same resource can have multiple events. String pk = String.format("%s-%s-%s-%s", evt.monkeyType().name(), evt.id(), region, evtTime); PutAttributesRequest putReq = new PutAttributesRequest(domain, pk, attrs); sdbClient().putAttributes(putReq); } /** * Find events. * * @param queryMap * the query map * @param after * the start time to query for all events after * @return the list */ protected List findEvents(Map queryMap, long after) { StringBuilder query = new StringBuilder( String.format("select * from `%s` where region = '%s'", domain, region)); for (Map.Entry pair : queryMap.entrySet()) { query.append(String.format(" and %s = '%s'", pair.getKey(), pair.getValue())); } query.append(String.format(" and eventTime > '%d'", after)); // always return with most recent record first query.append(" order by eventTime desc"); List list = new LinkedList(); SelectRequest request = new SelectRequest(query.toString()); request.setConsistentRead(Boolean.TRUE); SelectResult result = new SelectResult(); do { result = sdbClient().select(request.withNextToken(result.getNextToken())); for (Item item : result.getItems()) { Map fields = new HashMap(); Map res = new HashMap(); for (Attribute attr : item.getAttributes()) { if (Keys.KEYSET.contains(attr.getName())) { res.put(attr.getName(), attr.getValue()); } else { fields.put(attr.getName(), attr.getValue()); } } String eid = res.get(Keys.id.name()); String ereg = res.get(Keys.region.name()); MonkeyType monkeyType = valueToEnum(MonkeyType.class, res.get(Keys.monkeyType.name())); EventType eventType = valueToEnum(EventType.class, res.get(Keys.eventType.name())); long eventTime = Long.parseLong(res.get(Keys.eventTime.name())); list.add(new BasicRecorderEvent(monkeyType, eventType, ereg, eid, eventTime).addFields(fields)); } } while (result.getNextToken() != null); return list; } /** {@inheritDoc} */ @Override public List findEvents(Map query, Date after) { return findEvents(query, after.getTime()); } /** {@inheritDoc} */ @Override public List findEvents(MonkeyType monkeyType, Map query, Date after) { Map copy = new LinkedHashMap(query); copy.put(Keys.monkeyType.name(), enumToValue(monkeyType)); return findEvents(copy, after); } /** {@inheritDoc} */ @Override public List findEvents(MonkeyType monkeyType, EventType eventType, Map query, Date after) { Map copy = new LinkedHashMap(query); copy.put(Keys.monkeyType.name(), enumToValue(monkeyType)); copy.put(Keys.eventType.name(), enumToValue(eventType)); return findEvents(copy, after); } /** * Creates the SimpleDB domain, if it does not already exist. */ public void init() { try { if (this.region == null || this.region.equals("region-null")) { // This is a mock with an invalid region; avoid a slow timeout LOGGER.debug("Region=null; skipping SimpleDB domain creation"); return; } ListDomainsResult listDomains = sdbClient().listDomains(); for (String d : listDomains.getDomainNames()) { if (d.equals(domain)) { LOGGER.debug("SimpleDB domain found: {}", domain); return; } } LOGGER.info("Creating SimpleDB domain: {}", domain); CreateDomainRequest createDomainRequest = new CreateDomainRequest( domain); sdbClient().createDomain(createDomainRequest); } catch (AmazonClientException e) { LOGGER.warn("Error while trying to auto-create SimpleDB domain", e); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/RDSConformityClusterTracker.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity; import com.amazonaws.AmazonClientException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityClusterTracker; import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * The RDSConformityClusterTracker implementation in RDS (relational database). */ public class RDSConformityClusterTracker implements ConformityClusterTracker { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(RDSConformityClusterTracker.class); /** The table. */ private final String table; /** the jdbcTemplate */ JdbcTemplate jdbcTemplate = null; /** * Instantiates a new RDS db resource tracker. * */ public RDSConformityClusterTracker(String dbDriver, String dbUser, String dbPass, String dbUrl, String dbTable) { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName(dbDriver); dataSource.setJdbcUrl(dbUrl); dataSource.setUsername(dbUser); dataSource.setPassword(dbPass); dataSource.setMaximumPoolSize(2); this.jdbcTemplate = new JdbcTemplate(dataSource); this.table = dbTable; } /** * Instantiates a new RDS conformity cluster tracker. This constructor is intended * for unit testing. * */ public RDSConformityClusterTracker(JdbcTemplate jdbcTemplate, String table) { this.jdbcTemplate = jdbcTemplate; this.table = table; } public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } public Object value(String value) { return value == null ? Types.NULL : value; } public Object value(Date value) { return value == null ? Types.NULL : value.getTime(); } public Object value(boolean value) { return Boolean.toString(value); } public Object emailValue(String email) { if (StringUtils.isBlank(email)) return Types.NULL; if (email.equals("0")) return Types.NULL; return email; } /** {@inheritDoc} */ @Override public void addOrUpdate(Cluster cluster) { Cluster orig = getCluster(cluster.getName(), cluster.getRegion()); LOGGER.debug(String.format("Saving cluster %s to RDB table %s in region %s", cluster.getName(), cluster.getRegion(), table)); Map map = cluster.getFieldToValueMap(); String conformityJson; try { conformityJson = new ObjectMapper().writeValueAsString(conformitiesAsMap(cluster)); } catch (JsonProcessingException e) { LOGGER.error("ERROR generating conformities JSON when saving cluster " + cluster.getName() + ", " + cluster.getRegion(), e); return; } if (orig == null) { StringBuilder sb = new StringBuilder(); sb.append("insert into ").append(table); sb.append(" ("); sb.append(Cluster.CLUSTER).append(","); sb.append(Cluster.REGION).append(","); sb.append(Cluster.OWNER_EMAIL).append(","); sb.append(Cluster.IS_CONFORMING).append(","); sb.append(Cluster.IS_OPTEDOUT).append(","); sb.append(Cluster.UPDATE_TIMESTAMP).append(","); sb.append(Cluster.EXCLUDED_RULES).append(","); sb.append("conformities").append(","); sb.append(Cluster.CONFORMITY_RULES); sb.append(") values (?,?,?,?,?,?,?,?,?)"); LOGGER.debug(String.format("Insert statement is '%s'", sb)); this.jdbcTemplate.update(sb.toString(), value(map.get(Cluster.CLUSTER)), value(map.get(Cluster.REGION)), emailValue(map.get(Cluster.OWNER_EMAIL)), value(map.get(Cluster.IS_CONFORMING)), value(map.get(Cluster.IS_OPTEDOUT)), value(cluster.getUpdateTime()), value(map.get(Cluster.EXCLUDED_RULES)), value(conformityJson), value(map.get(Cluster.CONFORMITY_RULES))); } else { StringBuilder sb = new StringBuilder(); sb.append("update ").append(table).append(" set "); sb.append(Cluster.OWNER_EMAIL).append("=?,"); sb.append(Cluster.IS_CONFORMING).append("=?,"); sb.append(Cluster.IS_OPTEDOUT).append("=?,"); sb.append(Cluster.UPDATE_TIMESTAMP).append("=?,"); sb.append(Cluster.EXCLUDED_RULES).append("=?,"); sb.append("conformities").append("=?,"); sb.append(Cluster.CONFORMITY_RULES).append("=? where "); sb.append(Cluster.CLUSTER).append("=? and "); sb.append(Cluster.REGION).append("=?"); LOGGER.debug(String.format("Update statement is '%s'", sb)); this.jdbcTemplate.update(sb.toString(), emailValue(map.get(Cluster.OWNER_EMAIL)), value(map.get(Cluster.IS_CONFORMING)), value(map.get(Cluster.IS_OPTEDOUT)), value(cluster.getUpdateTime()), value(map.get(Cluster.EXCLUDED_RULES)), value(conformityJson), value(map.get(Cluster.CONFORMITY_RULES)), value(cluster.getName()), value(cluster.getRegion())); } LOGGER.debug("Successfully saved."); } private HashMap conformitiesAsMap(Cluster cluster) { HashMap map = new HashMap<>(); for(Conformity conformity : cluster.getConformties()) { map.put(conformity.getRuleId(), StringUtils.join(conformity.getFailedComponents(), ",")); } return map; } /** * Gets the clusters for a list of regions. If the regions parameter is empty, returns the clusters * for all regions. */ @Override public List getAllClusters(String... regions) { return getClusters(null, regions); } @Override public List getNonconformingClusters(String... regions) { return getClusters(false, regions); } @Override public Cluster getCluster(String clusterName, String region) { Validate.notEmpty(clusterName); Validate.notEmpty(region); StringBuilder query = new StringBuilder(); query.append(String.format("select * from %s where cluster = ? and region = ?", table)); LOGGER.info(String.format("Query is '%s'", query)); List clusters = jdbcTemplate.query(query.toString(), new String[] {clusterName, region}, new RowMapper() { public Cluster mapRow(ResultSet rs, int rowNum) throws SQLException { return mapResource(rs); } }); Validate.isTrue(clusters.size() <= 1); if (clusters.size() == 0) { LOGGER.info(String.format("Not found cluster with name %s in region %s", clusterName, region)); return null; } else { Cluster cluster = clusters.get(0); return cluster; } } private Cluster mapResource(ResultSet rs) throws SQLException { Map map = conformityMapFromJson(rs.getString("conformities")); map.put(Cluster.CLUSTER, rs.getString(Cluster.CLUSTER)); map.put(Cluster.REGION, rs.getString(Cluster.REGION)); map.put(Cluster.IS_CONFORMING, rs.getString(Cluster.IS_CONFORMING)); map.put(Cluster.IS_OPTEDOUT, rs.getString(Cluster.IS_OPTEDOUT)); String email = rs.getString(Cluster.OWNER_EMAIL); if (StringUtils.isBlank(email) || email.equals("0")) { email = null; } map.put(Cluster.OWNER_EMAIL, email); String updatedTimestamp = millisToFormattedDate(rs.getString(Cluster.UPDATE_TIMESTAMP)); if (updatedTimestamp != null) { map.put(Cluster.UPDATE_TIMESTAMP, updatedTimestamp); } map.put(Cluster.EXCLUDED_RULES, rs.getString(Cluster.EXCLUDED_RULES)); map.put(Cluster.CONFORMITY_RULES, rs.getString(Cluster.CONFORMITY_RULES)); return Cluster.parseFieldToValueMap(map); } private String millisToFormattedDate(String millisStr) { String datetime = null; try { long millis = Long.parseLong(millisStr); datetime = AWSResource.DATE_FORMATTER.print(millis); } catch(NumberFormatException nfe) { LOGGER.error(String.format("Error parsing datetime %s when reading from RDS", millisStr)); } return datetime; } private HashMap conformityMapFromJson(String json) throws SQLException { HashMap map = new HashMap<>(); if (json != null) { TypeReference> typeRef = new TypeReference>() {}; try { ObjectMapper mapper = new ObjectMapper(); map = mapper.readValue(json, typeRef); }catch(IOException ie) { String msg = "Error parsing conformities from result set"; LOGGER.error(msg, ie); throw new SQLException(msg); } } return map; } @Override public void deleteClusters(Cluster... clusters) { Validate.notNull(clusters); LOGGER.info(String.format("Deleting %d clusters", clusters.length)); for (Cluster cluster : clusters) { LOGGER.info(String.format("Deleting cluster %s", cluster.getName())); String stmt = String.format("delete from %s where %s=? and %s=?", table, Cluster.CLUSTER, Cluster.REGION); jdbcTemplate.update(stmt, cluster.getName(), cluster.getRegion()); LOGGER.info(String.format("Successfully deleted cluster %s", cluster.getName())); } } private List getClusters(Boolean conforming, String... regions) { Validate.notNull(regions); StringBuilder query = new StringBuilder(); query.append(String.format("select * from %s where cluster is not null and ", table)); boolean needsAnd = false; if (regions.length != 0) { query.append(String.format("region in ('%s') ", StringUtils.join(regions, "','"))); needsAnd = true; } if (conforming != null) { if (needsAnd) { query.append(" and "); } query.append(String.format("isConforming = '%s'", conforming)); } LOGGER.info(String.format("Query to retrieve clusters for regions %s is '%s'", StringUtils.join(regions, "','"), query.toString())); List clusters = jdbcTemplate.query(query.toString(), new RowMapper() { public Cluster mapRow(ResultSet rs, int rowNum) throws SQLException { return mapResource(rs); } }); LOGGER.info(String.format("Retrieved %d clusters from RDS DB in table %s and regions %s", clusters.size(), table, StringUtils.join(regions, "','"))); return clusters; } /** * Creates the RDS table, if it does not already exist. */ public void init() { try { LOGGER.info("Creating RDS table: {}", table); String sql = String.format("create table if not exists %s (" + " %s varchar(255)," + " %s varchar(25)," + " %s varchar(255)," + " %s varchar(10)," + " %s varchar(10)," + " %s BIGINT," + " %s varchar(4096)," + " %s varchar(4096)," + " %s varchar(4096) )", table, Cluster.CLUSTER, Cluster.REGION, Cluster.OWNER_EMAIL, Cluster.IS_CONFORMING, Cluster.IS_OPTEDOUT, Cluster.UPDATE_TIMESTAMP, Cluster.EXCLUDED_RULES, "conformities", Cluster.CONFORMITY_RULES); LOGGER.debug("Create SQL is: '{}'", sql); jdbcTemplate.execute(sql); } catch (AmazonClientException e) { LOGGER.warn("Error while trying to auto-create RDS table", e); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/SimpleDBConformityClusterTracker.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity; import com.amazonaws.services.simpledb.AmazonSimpleDB; import com.amazonaws.services.simpledb.model.Attribute; import com.amazonaws.services.simpledb.model.DeleteAttributesRequest; import com.amazonaws.services.simpledb.model.Item; import com.amazonaws.services.simpledb.model.PutAttributesRequest; import com.amazonaws.services.simpledb.model.ReplaceableAttribute; import com.amazonaws.services.simpledb.model.SelectRequest; import com.amazonaws.services.simpledb.model.SelectResult; import com.google.common.collect.Lists; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.ConformityClusterTracker; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * The ConformityResourceTracker implementation in SimpleDB. */ public class SimpleDBConformityClusterTracker implements ConformityClusterTracker { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDBConformityClusterTracker.class); /** The domain. */ private final String domain; /** The SimpleDB client. */ private final AmazonSimpleDB simpleDBClient; private static final int MAX_ATTR_SIZE = 1024; /** * Instantiates a new simple db cluster tracker for conformity monkey. * * @param awsClient * the AWS Client * @param domain * the domain */ public SimpleDBConformityClusterTracker(AWSClient awsClient, String domain) { Validate.notNull(awsClient); Validate.notNull(domain); this.domain = domain; this.simpleDBClient = awsClient.sdbClient(); } /** * Gets the SimpleDB client. * @return the SimpleDB client */ protected AmazonSimpleDB getSimpleDBClient() { return simpleDBClient; } /** {@inheritDoc} */ @Override public void addOrUpdate(Cluster cluster) { List attrs = new ArrayList(); Map fieldToValueMap = cluster.getFieldToValueMap(); for (Map.Entry entry : fieldToValueMap.entrySet()) { attrs.add(new ReplaceableAttribute(entry.getKey(), StringUtils.left(entry.getValue(), MAX_ATTR_SIZE), true)); } PutAttributesRequest putReqest = new PutAttributesRequest(domain, getSimpleDBItemName(cluster), attrs); LOGGER.debug(String.format("Saving cluster %s to SimpleDB domain %s", cluster.getName(), domain)); this.simpleDBClient.putAttributes(putReqest); LOGGER.debug("Successfully saved."); } /** * Gets the clusters for a list of regions. If the regions parameter is empty, returns the clusters * for all regions. */ @Override public List getAllClusters(String... regions) { return getClusters(null, regions); } @Override public List getNonconformingClusters(String... regions) { return getClusters(false, regions); } @Override public Cluster getCluster(String clusterName, String region) { Validate.notEmpty(clusterName); Validate.notEmpty(region); StringBuilder query = new StringBuilder(); query.append(String.format("select * from `%s` where cluster = '%s' and region = '%s'", domain, clusterName, region)); LOGGER.info(String.format("Query is to get the cluster is '%s'", query)); List items = querySimpleDBItems(query.toString()); Validate.isTrue(items.size() <= 1); if (items.size() == 0) { LOGGER.info(String.format("Not found cluster with name %s in region %s", clusterName, region)); return null; } else { Cluster cluster = null; try { cluster = parseCluster(items.get(0)); } catch (Exception e) { // Ignore the item that cannot be parsed. LOGGER.error(String.format("SimpleDB item %s cannot be parsed into a cluster.", items.get(0))); } return cluster; } } @Override public void deleteClusters(Cluster... clusters) { Validate.notNull(clusters); LOGGER.info(String.format("Deleting %d clusters", clusters.length)); for (Cluster cluster : clusters) { LOGGER.info(String.format("Deleting cluster %s", cluster.getName())); simpleDBClient.deleteAttributes(new DeleteAttributesRequest(domain, getSimpleDBItemName(cluster))); LOGGER.info(String.format("Successfully deleted cluster %s", cluster.getName())); } } private List getClusters(Boolean conforming, String... regions) { Validate.notNull(regions); List clusters = Lists.newArrayList(); StringBuilder query = new StringBuilder(); query.append(String.format("select * from `%s` where cluster is not null and ", domain)); boolean needsAnd = false; if (regions.length != 0) { query.append(String.format("region in ('%s') ", StringUtils.join(regions, "','"))); needsAnd = true; } if (conforming != null) { if (needsAnd) { query.append(" and "); } query.append(String.format("isConforming = '%s'", conforming)); } LOGGER.info(String.format("Query to retrieve clusters for regions %s is '%s'", StringUtils.join(regions, "','"), query.toString())); List items = querySimpleDBItems(query.toString()); for (Item item : items) { try { clusters.add(parseCluster(item)); } catch (Exception e) { // Ignore the item that cannot be parsed. LOGGER.error(String.format("SimpleDB item %s cannot be parsed into a cluster.", item), e); } } LOGGER.info(String.format("Retrieved %d clusters from SimpleDB in domain %s and regions %s", clusters.size(), domain, StringUtils.join(regions, "','"))); return clusters; } /** * Parses a SimpleDB item into a cluster. * @param item the item from SimpleDB * @return the cluster for the SimpleDB item */ protected Cluster parseCluster(Item item) { Map fieldToValue = new HashMap(); for (Attribute attr : item.getAttributes()) { String name = attr.getName(); String value = attr.getValue(); if (name != null && value != null) { fieldToValue.put(name, value); } } return Cluster.parseFieldToValueMap(fieldToValue); } /** * Gets the unique SimpleDB item name for a cluster. The subclass can override this * method to generate the item name differently. * @param cluster * @return the SimpleDB item name for the cluster */ protected String getSimpleDBItemName(Cluster cluster) { return String.format("%s-%s", cluster.getName(), cluster.getRegion()); } private List querySimpleDBItems(String query) { Validate.notNull(query); String nextToken = null; List items = new ArrayList(); do { SelectRequest request = new SelectRequest(query); request.setNextToken(nextToken); request.setConsistentRead(Boolean.TRUE); SelectResult result = this.simpleDBClient.select(request); items.addAll(result.getItems()); nextToken = result.getNextToken(); } while (nextToken != null); return items; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/crawler/AWSClusterCrawler.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.crawler; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.TagDescription; import com.amazonaws.services.autoscaling.model.Instance; import com.amazonaws.services.autoscaling.model.SuspendedProcess; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.ClusterCrawler; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Map; import java.util.Set; /** * The class implementing a crawler that gets the auto scaling groups from AWS. */ public class AWSClusterCrawler implements ClusterCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(AWSClusterCrawler.class); private static final String NS = "simianarmy.conformity.cluster"; /** The map from region to the aws client in the region. */ private final Map regionToAwsClient = Maps.newHashMap(); private final MonkeyConfiguration cfg; /** * Instantiates a new cluster crawler. * * @param regionToAwsClient * the map from region to the corresponding aws client for the region */ public AWSClusterCrawler(Map regionToAwsClient, MonkeyConfiguration cfg) { Validate.notNull(regionToAwsClient); Validate.notNull(cfg); for (Map.Entry entry : regionToAwsClient.entrySet()) { this.regionToAwsClient.put(entry.getKey(), entry.getValue()); } this.cfg = cfg; } /** * In this implementation, every auto scaling group is considered a cluster. * @param clusterNames * the cluster names * @return the list of clusters matching the names, when names are empty, return all clusters */ @Override public List clusters(String... clusterNames) { List list = Lists.newArrayList(); for (Map.Entry entry : regionToAwsClient.entrySet()) { String region = entry.getKey(); AWSClient awsClient = entry.getValue(); Set asgInstances = Sets.newHashSet(); LOGGER.info(String.format("Crawling clusters in region %s", region)); for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups(clusterNames)) { List instances = Lists.newArrayList(); for (Instance instance : asg.getInstances()) { instances.add(instance.getInstanceId()); asgInstances.add(instance.getInstanceId()); } com.netflix.simianarmy.conformity.AutoScalingGroup conformityAsg = new com.netflix.simianarmy.conformity.AutoScalingGroup( asg.getAutoScalingGroupName(), instances.toArray(new String[instances.size()])); for (SuspendedProcess sp : asg.getSuspendedProcesses()) { if ("AddToLoadBalancer".equals(sp.getProcessName())) { LOGGER.info(String.format("ASG %s is suspended: %s", asg.getAutoScalingGroupName(), asg.getSuspendedProcesses())); conformityAsg.setSuspended(true); } } Cluster cluster = new Cluster(asg.getAutoScalingGroupName(), region, conformityAsg); List tagDescriptions = asg.getTags(); for (TagDescription tagDescription : tagDescriptions) { if ( BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY.equalsIgnoreCase(tagDescription.getKey()) ) { String value = tagDescription.getValue(); if (value != null) { cluster.setOwnerEmail(value); } } } updateCluster(cluster); list.add(cluster); } //Cluster containing all solo instances Set instances = Sets.newHashSet(); for (com.amazonaws.services.ec2.model.Instance awsInstance : awsClient.describeInstances()) { if (!asgInstances.contains(awsInstance.getInstanceId())) { LOGGER.info(String.format("Adding instance %s to soloInstances cluster.", awsInstance.getInstanceId())); instances.add(awsInstance.getInstanceId()); } } //Only create cluster if we have solo instances. if (!instances.isEmpty()) { Cluster cluster = new Cluster("SoloInstances", region, instances); updateCluster(cluster); list.add(cluster); } } return list; } private void updateCluster(Cluster cluster) { updateExcludedConformityRules(cluster); cluster.setOwnerEmail(getOwnerEmailForCluster(cluster)); String prop = String.format("simianarmy.conformity.cluster.%s.optedOut", cluster.getName()); if (cfg.getBoolOrElse(prop, false)) { LOGGER.info(String.format("Cluster %s is opted out of Conformity Monkey.", cluster.getName())); cluster.setOptOutOfConformity(true); } else { cluster.setOptOutOfConformity(false); } } /** * Gets the owner email from the monkey configuration. * @param cluster * the cluster * @return the owner email if it is defined in the configuration, null otherwise. */ @Override public String getOwnerEmailForCluster(Cluster cluster) { String prop = String.format("%s.%s.ownerEmail", NS, cluster.getName()); String ownerEmail = cfg.getStr(prop); if (ownerEmail == null) { ownerEmail = cluster.getOwnerEmail(); if (ownerEmail == null) { LOGGER.info(String.format("No owner email is found for cluster %s in configuration " + "%s or tag %s.", cluster.getName(), prop, BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY)); } else { LOGGER.info(String.format("Found owner email %s for cluster %s in tag %s.", ownerEmail, cluster.getName(), BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY)); return ownerEmail; } } else { LOGGER.info(String.format("Found owner email %s for cluster %s in configuration %s.", ownerEmail, cluster.getName(), prop)); } return ownerEmail; } @Override public void updateExcludedConformityRules(Cluster cluster) { String prop = String.format("%s.%s.excludedRules", NS, cluster.getName()); String excludedRules = cfg.getStr(prop); if (StringUtils.isNotBlank(excludedRules)) { LOGGER.info(String.format("Excluded rules for cluster %s are : %s", cluster.getName(), excludedRules)); cluster.excludeRules(StringUtils.split(excludedRules, ",")); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/BasicConformityEurekaClient.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.DiscoveryClient; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Set; /** * The class implementing a client to access Eureda for getting instance information that is used * by Conformity Monkey. */ public class BasicConformityEurekaClient implements ConformityEurekaClient { private static final Logger LOGGER = LoggerFactory.getLogger(BasicConformityEurekaClient.class); private final DiscoveryClient discoveryClient; /** * Constructor. * @param discoveryClient the client to access Discovery/Eureka service. */ public BasicConformityEurekaClient(DiscoveryClient discoveryClient) { this.discoveryClient = discoveryClient; } @Override public boolean hasHealthCheckUrl(String region, String instanceId) { List instanceInfos = discoveryClient.getInstancesById(instanceId); for (InstanceInfo info : instanceInfos) { Set healthCheckUrls = info.getHealthCheckUrls(); if (healthCheckUrls != null && !healthCheckUrls.isEmpty()) { return true; } } return false; } @Override public boolean hasStatusUrl(String region, String instanceId) { List instanceInfos = discoveryClient.getInstancesById(instanceId); for (InstanceInfo info : instanceInfos) { String statusPageUrl = info.getStatusPageUrl(); if (!StringUtils.isEmpty(statusPageUrl)) { return true; } } return false; } @Override public boolean isHealthy(String region, String instanceId) { List instanceInfos = discoveryClient.getInstancesById(instanceId); if (instanceInfos.isEmpty()) { LOGGER.info(String.format("Instance %s is not registered in Eureka in region %s.", instanceId, region)); return false; } else { for (InstanceInfo info : instanceInfos) { InstanceInfo.InstanceStatus status = info.getStatus(); if (!status.equals(InstanceInfo.InstanceStatus.UP) && !status.equals(InstanceInfo.InstanceStatus.STARTING)) { LOGGER.info(String.format("Instance %s is not healthy in Eureka with status %s.", instanceId, status.name())); return false; } } } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/ConformityEurekaClient.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; /** * The interface for a client to access Eureka service to get the status of instances for Conformity Monkey. */ public interface ConformityEurekaClient { /** * Checks whether an instance has health check url in Eureka. * @param region the region of the instance * @param instanceId the instance id * @return true if the instance has health check url in Eureka, false otherwise. */ boolean hasHealthCheckUrl(String region, String instanceId); /** * Checks whether an instance has status url in Eureka. * @param region the region of the instance * @param instanceId the instance id * @return true if the instance has status url in Eureka, false otherwise. */ boolean hasStatusUrl(String region, String instanceId); /** * Checks whether an instance is healthy in Eureka. * @param region the region of the instance * @param instanceId the instance id * @return true if the instance is healthy in Eureka, false otherwise. */ boolean isHealthy(String region, String instanceId); } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/CrossZoneLoadBalancing.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import com.netflix.simianarmy.client.MonkeyRestClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerAttributes; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityRule; /** * The class implementing a conformity rule that checks if the cross-zone load balancing is enabled * for all cluster ELBs. */ public class CrossZoneLoadBalancing implements ConformityRule { private static final Logger LOGGER = LoggerFactory.getLogger(CrossZoneLoadBalancing.class); private final Map regionToAwsClient = Maps.newHashMap(); private AWSCredentialsProvider awsCredentialsProvider; private static final String RULE_NAME = "CrossZoneLoadBalancing"; private static final String REASON = "Cross-zone load balancing is disabled"; /** * Constructs an instance with the default AWS credentials provider chain. * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain */ public CrossZoneLoadBalancing() { this(new DefaultAWSCredentialsProviderChain()); } /** * Constructs an instance with the passed AWS Credential Provider. * @param awsCredentialsProvider */ public CrossZoneLoadBalancing(AWSCredentialsProvider awsCredentialsProvider) { this.awsCredentialsProvider = awsCredentialsProvider; } @Override public Conformity check(Cluster cluster) { Collection failedComponents = Lists.newArrayList(); for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { try { for (String lbName : getLoadBalancerNamesForAsg(cluster.getRegion(), asg.getName())) { if (!isCrossZoneLoadBalancingEnabled(cluster.getRegion(), lbName)) { LOGGER.info(String.format("ELB %s in %s does not have cross-zone load balancing enabled", lbName, cluster.getRegion())); failedComponents.add(lbName); } } } catch (MonkeyRestClient.DataReadException e) { LOGGER.error(String.format("Transient error reading ELB for %s in %s - skipping this check", asg.getName(), cluster.getRegion()), e); } } return new Conformity(getName(), failedComponents); } /** * Gets the cross-zone load balancing option for an ELB. Can be overridden in subclasses. * @param region the region * @param lbName the ELB name * @return {@code true} if cross-zone load balancing is enabled */ protected boolean isCrossZoneLoadBalancingEnabled(String region, String lbName) { LoadBalancerAttributes attrs = getAwsClient(region).describeElasticLoadBalancerAttributes(lbName); return attrs.getCrossZoneLoadBalancing().isEnabled(); } @Override public String getName() { return RULE_NAME; } @Override public String getNonconformingReason() { return REASON; } /** * Gets the load balancer names of an ASG. Can be overridden in subclasses. * @param region the region * @param asgName the ASG name * @return the list of load balancer names */ protected List getLoadBalancerNamesForAsg(String region, String asgName) { List asgs = getAwsClient(region).describeAutoScalingGroups(asgName); if (asgs.isEmpty()) { LOGGER.error(String.format("Not found ASG with name %s", asgName)); return Collections.emptyList(); } else { return asgs.get(0).getLoadBalancerNames(); } } private AWSClient getAwsClient(String region) { AWSClient awsClient = regionToAwsClient.get(region); if (awsClient == null) { awsClient = new AWSClient(region, awsCredentialsProvider); regionToAwsClient.put(region, awsClient); } return awsClient; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceHasHealthCheckUrl.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import com.google.common.collect.Lists; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityRule; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; /** * The class implementing a conformity rule that checks if all instances in a cluster has health check url * in Discovery/Eureka. */ public class InstanceHasHealthCheckUrl implements ConformityRule { private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasHealthCheckUrl.class); private static final String RULE_NAME = "InstanceHasHealthCheckUrl"; private static final String REASON = "Health check url not defined"; private final ConformityEurekaClient conformityEurekaClient; /** * Constructor. * @param conformityEurekaClient * the client to access the Discovery/Eureka service for checking the status of instances. */ public InstanceHasHealthCheckUrl(ConformityEurekaClient conformityEurekaClient) { Validate.notNull(conformityEurekaClient); this.conformityEurekaClient = conformityEurekaClient; } @Override public Conformity check(Cluster cluster) { Collection failedComponents = Lists.newArrayList(); for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { if (asg.isSuspended()) { continue; } for (String instance : asg.getInstances()) { if (!conformityEurekaClient.hasHealthCheckUrl(cluster.getRegion(), instance)) { LOGGER.info(String.format("Instance %s does not have health check url in discovery.", instance)); failedComponents.add(instance); } } } return new Conformity(getName(), failedComponents); } @Override public String getName() { return RULE_NAME; } @Override public String getNonconformingReason() { return REASON; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceHasStatusUrl.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import com.google.common.collect.Lists; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityRule; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; /** * The class implementing a conformity rule that checks if all instances in a cluster has status url. */ public class InstanceHasStatusUrl implements ConformityRule { private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasStatusUrl.class); private static final String RULE_NAME = "InstanceHasStatusUrl"; private static final String REASON = "Status url not defined"; private final ConformityEurekaClient conformityEurekaClient; /** * Constructor. * @param conformityEurekaClient * the client to access the Discovery/Eureka service for checking the status of instances. */ public InstanceHasStatusUrl(ConformityEurekaClient conformityEurekaClient) { Validate.notNull(conformityEurekaClient); this.conformityEurekaClient = conformityEurekaClient; } @Override public Conformity check(Cluster cluster) { Collection failedComponents = Lists.newArrayList(); for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { if (asg.isSuspended()) { continue; } for (String instance : asg.getInstances()) { if (!conformityEurekaClient.hasStatusUrl(cluster.getRegion(), instance)) { LOGGER.info(String.format("Instance %s does not have a status page url in discovery.", instance)); failedComponents.add(instance); } } } return new Conformity(getName(), failedComponents); } @Override public String getName() { return RULE_NAME; } @Override public String getNonconformingReason() { return REASON; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceInSecurityGroup.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.ec2.model.GroupIdentifier; import com.amazonaws.services.ec2.model.Instance; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityRule; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.List; import java.util.Map; /** * The class implementing a conformity rule that checks whether or not all instances in a cluster are in * specific security groups. */ public class InstanceInSecurityGroup implements ConformityRule { private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasStatusUrl.class); private static final String RULE_NAME = "InstanceInSecurityGroup"; private final String reason; private final Collection requiredSecurityGroupNames = Sets.newHashSet(); private AWSCredentialsProvider awsCredentialsProvider; /** * Constructor. * @param requiredSecurityGroupNames * The security group names that are required to have for every instance of a cluster. */ public InstanceInSecurityGroup(String... requiredSecurityGroupNames) { this(new DefaultAWSCredentialsProviderChain(), requiredSecurityGroupNames); } /** * Constructor. * @param awsCredentialsProvider * The AWS credentials provider * @param requiredSecurityGroupNames * The security group names that are required to have for every instance of a cluster. */ public InstanceInSecurityGroup(AWSCredentialsProvider awsCredentialsProvider, String... requiredSecurityGroupNames) { this.awsCredentialsProvider = awsCredentialsProvider; Validate.notNull(requiredSecurityGroupNames); for (String sgName : requiredSecurityGroupNames) { Validate.notNull(sgName); this.requiredSecurityGroupNames.add(sgName.trim()); } this.reason = String.format("Instances are not part of security groups (%s)", StringUtils.join(this.requiredSecurityGroupNames, ",")); } @Override public Conformity check(Cluster cluster) { List instanceIds = Lists.newArrayList(); for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { instanceIds.addAll(asg.getInstances()); } Collection failedComponents = Lists.newArrayList(); if (instanceIds.size() != 0) { Map> instanceIdToSecurityGroup = getInstanceSecurityGroups( cluster.getRegion(), instanceIds.toArray(new String[instanceIds.size()])); for (Map.Entry> entry : instanceIdToSecurityGroup.entrySet()) { String instanceId = entry.getKey(); if (!checkSecurityGroups(entry.getValue())) { LOGGER.info(String.format("Instance %s does not have all required security groups", instanceId)); failedComponents.add(instanceId); } } } return new Conformity(getName(), failedComponents); } @Override public String getName() { return RULE_NAME; } @Override public String getNonconformingReason() { return reason; } /** * Checks whether the collection of security group names are valid. The default implementation here is to check * whether the security groups contain the required security groups. The method can be overridden for different * rules. * @param sgNames * The collection of security group names * @return * true if the security group names are valid, false otherwise. */ protected boolean checkSecurityGroups(Collection sgNames) { for (String requiredSg : requiredSecurityGroupNames) { if (!sgNames.contains(requiredSg)) { LOGGER.info(String.format("Required security group %s is not found.", requiredSg)); return false; } } return true; } /** * Gets the security groups for a list of instance ids of the same region. The default implementation * is using an AWS client. The method can be overridden in subclasses to get the security groups differently. * @param region * the region of the instances * @param instanceIds * the instance ids, all instances should be in the same region. * @return * the map from instance id to the list of security group names the instance has */ protected Map> getInstanceSecurityGroups(String region, String... instanceIds) { Map> result = Maps.newHashMap(); if (instanceIds == null || instanceIds.length == 0) { return result; } AWSClient awsClient = new AWSClient(region, awsCredentialsProvider); for (Instance instance : awsClient.describeInstances(instanceIds)) { // Ignore instances that are in VPC if (StringUtils.isNotEmpty(instance.getVpcId())) { LOGGER.info(String.format("Instance %s is in VPC and is ignored.", instance.getInstanceId())); continue; } if (!"running".equals(instance.getState().getName())) { LOGGER.info(String.format("Instance %s is not running, state is %s.", instance.getInstanceId(), instance.getState().getName())); continue; } List sgs = Lists.newArrayList(); for (GroupIdentifier groupId : instance.getSecurityGroups()) { sgs.add(groupId.getGroupName()); } result.put(instance.getInstanceId(), sgs); } return result; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceInVPC.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.ec2.model.Instance; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityRule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; /** * The class implements a conformity rule to check an instance is in a virtual private cloud. */ public class InstanceInVPC implements ConformityRule { private static final Logger LOGGER = LoggerFactory.getLogger(InstanceInVPC.class); private final Map regionToAwsClient = Maps.newHashMap(); private AWSCredentialsProvider awsCredentialsProvider; private static final String RULE_NAME = "InstanceInVPC"; private static final String REASON = "VPC_ID not defined"; /** * Constructs an instance with the default AWS credentials provider chain. * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain */ public InstanceInVPC() { this(new DefaultAWSCredentialsProviderChain()); } /** * Constructs an instance with the passed AWS credentials provider. * @param awsCredentialsProvider * The AWS credentials provider */ public InstanceInVPC(AWSCredentialsProvider awsCredentialsProvider) { this.awsCredentialsProvider = awsCredentialsProvider; } @Override public Conformity check(Cluster cluster) { Collection failedComponents = Lists.newArrayList(); //check all instances Set failedInstances = checkInstancesInVPC(cluster.getRegion(), cluster.getSoloInstances()); failedComponents.addAll(failedInstances); //check asg instances for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { if (asg.isSuspended()) { continue; } Set asgFailedInstances = checkInstancesInVPC(cluster.getRegion(), asg.getInstances()); failedComponents.addAll(asgFailedInstances); } return new Conformity(getName(), failedComponents); } @Override public String getName() { return RULE_NAME; } @Override public String getNonconformingReason() { return REASON; } private AWSClient getAwsClient(String region) { AWSClient awsClient = regionToAwsClient.get(region); if (awsClient == null) { awsClient = new AWSClient(region, awsCredentialsProvider); regionToAwsClient.put(region, awsClient); } return awsClient; } private Set checkInstancesInVPC(String region, Collection instances) { Set failedInstances = Sets.newHashSet(); for (String instanceId : instances) { for (Instance awsInstance : getAWSInstances(region, instanceId)) { if (awsInstance.getVpcId() == null) { LOGGER.info(String.format("Instance %s is not in a virtual private cloud", instanceId)); failedInstances.add(instanceId); } } } return failedInstances; } /** * Gets the list of AWS instances. Can be overridden * @param region the region * @param instanceId the instance id. * @return the list of the AWS instances with the given id. */ protected List getAWSInstances(String region, String instanceId) { AWSClient awsClient = getAwsClient(region); return awsClient.describeInstances(instanceId); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceIsHealthyInEureka.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import com.google.common.collect.Lists; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityRule; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; /** * The class implements a conformity rule to check if all instances in the cluster are healthy in Discovery. */ public class InstanceIsHealthyInEureka implements ConformityRule { private static final Logger LOGGER = LoggerFactory.getLogger(InstanceIsHealthyInEureka.class); private static final String RULE_NAME = "InstanceIsHealthyInEureka"; private static final String REASON = "Instances are not 'UP' in Eureka."; private final ConformityEurekaClient conformityEurekaClient; /** * Constructor. * @param conformityEurekaClient * the client to access the Discovery/Eureka service for checking the status of instances. */ public InstanceIsHealthyInEureka(ConformityEurekaClient conformityEurekaClient) { Validate.notNull(conformityEurekaClient); this.conformityEurekaClient = conformityEurekaClient; } @Override public Conformity check(Cluster cluster) { Collection failedComponents = Lists.newArrayList(); for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { // ignore suspended ASGs if (asg.isSuspended()) { LOGGER.info(String.format("ASG %s is suspended, ignore.", asg.getName())); continue; } for (String instance : asg.getInstances()) { if (!conformityEurekaClient.isHealthy(cluster.getRegion(), instance)) { LOGGER.info(String.format("Instance %s is not healthy in Eureka.", instance)); failedComponents.add(instance); } } } return new Conformity(getName(), failedComponents); } @Override public String getName() { return RULE_NAME; } @Override public String getNonconformingReason() { return REASON; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/InstanceTooOld.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.ec2.model.Instance; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityRule; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.List; import java.util.Map; /** * The class implementing a conformity rule that checks if there are instances that are older than certain days. * Instances are not considered to be permanent in the cloud, so sometimes having too old instances could indicate * potential issues. */ public class InstanceTooOld implements ConformityRule { private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasStatusUrl.class); private static final String RULE_NAME = "InstanceTooOld"; private final String reason; private final int instanceAgeThreshold; private AWSCredentialsProvider awsCredentialsProvider; /** * Constructor. * @param instanceAgeThreshold * The age in days that makes an instance be considered too old. */ public InstanceTooOld(int instanceAgeThreshold) { this(new DefaultAWSCredentialsProviderChain(), instanceAgeThreshold); } /** * Constructor. * @param awsCredentialsProvider * The AWS credentials provider * @param instanceAgeThreshold * The age in days that makes an instance be considered too old. */ public InstanceTooOld(AWSCredentialsProvider awsCredentialsProvider, int instanceAgeThreshold) { this.awsCredentialsProvider = awsCredentialsProvider; Validate.isTrue(instanceAgeThreshold > 0); this.instanceAgeThreshold = instanceAgeThreshold; this.reason = String.format("Instances are older than %d days", instanceAgeThreshold); } @Override public Conformity check(Cluster cluster) { List instanceIds = Lists.newArrayList(); for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { instanceIds.addAll(asg.getInstances()); } Map instanceIdToLaunchTime = getInstanceLaunchTimes( cluster.getRegion(), instanceIds.toArray(new String[instanceIds.size()])); Collection failedComponents = Lists.newArrayList(); long creationTimeThreshold = DateTime.now().minusDays(instanceAgeThreshold).getMillis(); for (Map.Entry entry : instanceIdToLaunchTime.entrySet()) { String instanceId = entry.getKey(); if (creationTimeThreshold > entry.getValue()) { LOGGER.info(String.format("Instance %s was created more than %d days ago", instanceId, instanceAgeThreshold)); failedComponents.add(instanceId); } } return new Conformity(getName(), failedComponents); } @Override public String getName() { return RULE_NAME; } @Override public String getNonconformingReason() { return reason; } /** * Gets the launch time (in milliseconds) for a list of instance ids of the same region. The default * implementation is using an AWS client. The method can be overridden in subclasses to get the instance * launch times differently. * @param region * the region of the instances * @param instanceIds * the instance ids, all instances should be in the same region. * @return * the map from instance id to the launch time in milliseconds */ protected Map getInstanceLaunchTimes(String region, String... instanceIds) { Map result = Maps.newHashMap(); if (instanceIds == null || instanceIds.length == 0) { return result; } AWSClient awsClient = new AWSClient(region, awsCredentialsProvider); for (Instance instance : awsClient.describeInstances(instanceIds)) { if (instance.getLaunchTime() != null) { result.put(instance.getInstanceId(), instance.getLaunchTime().getTime()); } else { LOGGER.warn(String.format("No launch time found for instance %s", instance.getInstanceId())); } } return result; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/conformity/rule/SameZonesInElbAndAsg.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.conformity.rule; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityRule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; /** * The class implementing a conformity rule that checks if the zones in ELB and ASG are the same. */ public class SameZonesInElbAndAsg implements ConformityRule { private static final Logger LOGGER = LoggerFactory.getLogger(InstanceHasStatusUrl.class); private final Map regionToAwsClient = Maps.newHashMap(); private AWSCredentialsProvider awsCredentialsProvider; private static final String RULE_NAME = "SameZonesInElbAndAsg"; private static final String REASON = "Availability zones of ELB and ASG are different"; /** * Constructs an instance with the default AWS credentials provider chain. * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain */ public SameZonesInElbAndAsg() { this(new DefaultAWSCredentialsProviderChain()); } /** * Constructs an instance with the passed AWS Credential Provider. * @param awsCredentialsProvider */ public SameZonesInElbAndAsg(AWSCredentialsProvider awsCredentialsProvider) { this.awsCredentialsProvider = awsCredentialsProvider; } @Override public Conformity check(Cluster cluster) { List asgNames = Lists.newArrayList(); for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { asgNames.add(asg.getName()); } Collection failedComponents = Lists.newArrayList(); for (AutoScalingGroup asg : cluster.getAutoScalingGroups()) { List asgZones = getAvailabilityZonesForAsg(cluster.getRegion(), asg.getName()); for (String lbName : getLoadBalancerNamesForAsg(cluster.getRegion(), asg.getName())) { List lbZones = getAvailabilityZonesForLoadBalancer(cluster.getRegion(), lbName); if (!haveSameZones(asgZones, lbZones)) { LOGGER.info(String.format("ASG %s and ELB %s do not have the same availability zones", asgZones, lbZones)); failedComponents.add(lbName); } } } return new Conformity(getName(), failedComponents); } @Override public String getName() { return RULE_NAME; } @Override public String getNonconformingReason() { return REASON; } /** * Gets the load balancer names of an ASG. Can be overridden in subclasses. * @param region the region * @param asgName the ASG name * @return the list of load balancer names */ protected List getLoadBalancerNamesForAsg(String region, String asgName) { List asgs = getAwsClient(region).describeAutoScalingGroups(asgName); if (asgs.isEmpty()) { LOGGER.error(String.format("Not found ASG with name %s", asgName)); return Collections.emptyList(); } else { return asgs.get(0).getLoadBalancerNames(); } } /** * Gets the list of availability zones for an ASG. Can be overridden in subclasses. * @param region the region * @param asgName the ASG name. * @return the list of the availability zones that the ASG has. */ protected List getAvailabilityZonesForAsg(String region, String asgName) { List asgs = getAwsClient(region).describeAutoScalingGroups(asgName); if (asgs.isEmpty()) { LOGGER.error(String.format("Not found ASG with name %s", asgName)); return null; } else { return asgs.get(0).getAvailabilityZones(); } } /** * Gets the list of availability zones for a load balancer. Can be overridden in subclasses. * @param region the region * @param lbName the load balancer name. * @return the list of the availability zones that the load balancer has. */ protected List getAvailabilityZonesForLoadBalancer(String region, String lbName) { List lbs = getAwsClient(region).describeElasticLoadBalancers(lbName); if (lbs.isEmpty()) { LOGGER.error(String.format("Not found load balancer with name %s", lbName)); return null; } else { return lbs.get(0).getAvailabilityZones(); } } private AWSClient getAwsClient(String region) { AWSClient awsClient = regionToAwsClient.get(region); if (awsClient == null) { awsClient = new AWSClient(region, awsCredentialsProvider); regionToAwsClient.put(region, awsClient); } return awsClient; } private boolean haveSameZones(List zones1, List zones2) { if (zones1 == null || zones2 == null) { return true; } if (zones1.size() != zones1.size()) { return false; } for (String zone : zones1) { if (!zones2.contains(zone)) { return false; } } for (String zone : zones2) { if (!zones1.contains(zone)) { return false; } } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/ASGJanitor.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.janitor.AbstractJanitor; /** * The Janitor responsible for ASG cleanup. */ public class ASGJanitor extends AbstractJanitor { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJanitor.class); private final AWSClient awsClient; /** * Constructor. * @param awsClient the AWS client * @param ctx the context */ public ASGJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) { super(ctx, AWSResourceType.ASG); Validate.notNull(awsClient); this.awsClient = awsClient; } @Override protected void postMark(Resource resource) { } @Override protected void cleanup(Resource resource) { LOGGER.info(String.format("Deleting ASG %s", resource.getId())); awsClient.deleteAutoScalingGroup(resource.getId()); } @Override protected void postCleanup(Resource resource) { } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/EBSSnapshotJanitor.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.janitor.AbstractJanitor; /** * The Janitor responsible for EBS snapshot cleanup. */ public class EBSSnapshotJanitor extends AbstractJanitor { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EBSSnapshotJanitor.class); private final AWSClient awsClient; /** * Constructor. * @param awsClient the AWS client * @param ctx the context */ public EBSSnapshotJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) { super(ctx, AWSResourceType.EBS_SNAPSHOT); Validate.notNull(awsClient); this.awsClient = awsClient; } @Override protected void postMark(Resource resource) { } @Override protected void cleanup(Resource resource) { LOGGER.info(String.format("Deleting EBS snapshot %s", resource.getId())); awsClient.deleteSnapshot(resource.getId()); } @Override protected void postCleanup(Resource resource) { } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/EBSVolumeJanitor.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.janitor.AbstractJanitor; /** * The Janitor responsible for EBS volume cleanup. */ public class EBSVolumeJanitor extends AbstractJanitor { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EBSVolumeJanitor.class); private final AWSClient awsClient; /** * Constructor. * @param awsClient the AWS client * @param ctx the context */ public EBSVolumeJanitor(AWSClient awsClient, AbstractJanitor.Context ctx) { super(ctx, AWSResourceType.EBS_VOLUME); Validate.notNull(awsClient); this.awsClient = awsClient; } @Override protected void postMark(Resource resource) { } @Override protected void cleanup(Resource resource) { LOGGER.info(String.format("Deleting EBS volume %s", resource.getId())); awsClient.deleteVolume(resource.getId()); } @Override protected void postCleanup(Resource resource) { } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/ELBJanitor.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.janitor.AbstractJanitor; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The Janitor responsible for elastic load balancer cleanup. */ public class ELBJanitor extends AbstractJanitor { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ELBJanitor.class); private final AWSClient awsClient; /** * Constructor. * @param awsClient the AWS client * @param ctx the context */ public ELBJanitor(AWSClient awsClient, Context ctx) { super(ctx, AWSResourceType.ELB); Validate.notNull(awsClient); this.awsClient = awsClient; } @Override protected void postMark(Resource resource) { } @Override protected void cleanup(Resource resource) { LOGGER.info(String.format("Deleting ELB %s", resource.getId())); awsClient.deleteElasticLoadBalancer(resource.getId()); // delete any DNS records attached to this ELB String dnsNames = resource.getAdditionalField("referencedDNS"); String dnsTypes = resource.getAdditionalField("referencedDNSTypes"); String dnsZones = resource.getAdditionalField("referencedDNSZones"); if (StringUtils.isNotBlank(dnsNames) && StringUtils.isNotBlank(dnsTypes) && StringUtils.isNotBlank(dnsZones)) { String[] dnsNamesSplit = StringUtils.split(dnsNames,','); String[] dnsTypesSplit = StringUtils.split(dnsTypes,','); String[] dnsZonesSplit = StringUtils.split(dnsZones,','); if (dnsNamesSplit.length != dnsTypesSplit.length) { LOGGER.error(String.format("DNS Name count does not match DNS Type count, aborting DNS delete for ELB %s"), resource.getId()); LOGGER.error(String.format("DNS Names found but not deleted: %s for ELB %s"), dnsNames, resource.getId()); return; } if (dnsNamesSplit.length != dnsZonesSplit.length) { LOGGER.error(String.format("DNS Name count does not match DNS Zone count, aborting DNS delete for ELB %s"), resource.getId()); LOGGER.error(String.format("DNS Names found but not deleted: %s for ELB %s"), dnsNames, resource.getId()); return; } for(int i=0; i getResources(ResourceType resourceType, CleanupState state, String resourceRegion) { Validate.notEmpty(resourceRegion); StringBuilder query = new StringBuilder(); ArrayList args = new ArrayList<>(); query.append(String.format("select * from %s where ", table)); if (resourceType != null) { query.append("resourceType=? and "); args.add(resourceType.toString()); } if (state != null) { query.append("state=? and "); args.add(state.toString()); } query.append("region=?"); args.add(resourceRegion); LOGGER.debug(String.format("Query is '%s'", query)); List resources = jdbcTemplate.query(query.toString(), args.toArray(), new RowMapper() { public Resource mapRow(ResultSet rs, int rowNum) throws SQLException { return mapResource(rs); } }); return resources; } private Resource mapResource(ResultSet rs) throws SQLException { String json = rs.getString("additionalFields"); Resource resource = null; try { // put additional fields Map map = new HashMap<>(); if (json != null) { TypeReference> typeRef = new TypeReference>() {}; map = new ObjectMapper().readValue(json, typeRef); } // put everything else map.put(AWSResource.FIELD_RESOURCE_ID, rs.getString(AWSResource.FIELD_RESOURCE_ID)); map.put(AWSResource.FIELD_RESOURCE_TYPE, rs.getString(AWSResource.FIELD_RESOURCE_TYPE)); map.put(AWSResource.FIELD_REGION, rs.getString(AWSResource.FIELD_REGION)); map.put(AWSResource.FIELD_DESCRIPTION, rs.getString(AWSResource.FIELD_DESCRIPTION)); map.put(AWSResource.FIELD_STATE, rs.getString(AWSResource.FIELD_STATE)); map.put(AWSResource.FIELD_TERMINATION_REASON, rs.getString(AWSResource.FIELD_TERMINATION_REASON)); map.put(AWSResource.FIELD_OPT_OUT_OF_JANITOR, rs.getString(AWSResource.FIELD_OPT_OUT_OF_JANITOR)); String email = rs.getString(AWSResource.FIELD_OWNER_EMAIL); if (StringUtils.isBlank(email) || email.equals("0")) { email = null; } map.put(AWSResource.FIELD_OWNER_EMAIL, email); String expectedTerminationTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_EXPECTED_TERMINATION_TIME)); String actualTerminationTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_ACTUAL_TERMINATION_TIME)); String notificationTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_NOTIFICATION_TIME)); String launchTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_LAUNCH_TIME)); String markTime = millisToFormattedDate(rs.getString(AWSResource.FIELD_MARK_TIME)); if (expectedTerminationTime != null) { map.put(AWSResource.FIELD_EXPECTED_TERMINATION_TIME, expectedTerminationTime); } if (actualTerminationTime != null) { map.put(AWSResource.FIELD_ACTUAL_TERMINATION_TIME, actualTerminationTime); } if (notificationTime != null) { map.put(AWSResource.FIELD_NOTIFICATION_TIME, notificationTime); } if (launchTime != null) { map.put(AWSResource.FIELD_LAUNCH_TIME, launchTime); } if (markTime != null) { map.put(AWSResource.FIELD_MARK_TIME, markTime); } resource = AWSResource.parseFieldtoValueMap(map); }catch(IOException ie) { String msg = "Error parsing resource from result set"; LOGGER.error(msg, ie); throw new SQLException(msg); } return resource; } private String millisToFormattedDate(String millisStr) { String datetime = null; try { long millis = Long.parseLong(millisStr); datetime = AWSResource.DATE_FORMATTER.print(millis); } catch(NumberFormatException nfe) { LOGGER.error(String.format("Error parsing datetime %s when reading from RDS", millisStr)); } return datetime; } @Override public Resource getResource(String resourceId) { Validate.notEmpty(resourceId); StringBuilder query = new StringBuilder(); query.append(String.format("select * from %s where resourceId=?", table)); LOGGER.debug(String.format("Query is '%s'", query)); List resources = jdbcTemplate.query(query.toString(), new String[]{resourceId}, new RowMapper() { public Resource mapRow(ResultSet rs, int rowNum) throws SQLException { return mapResource(rs); } }); Resource resource = null; Validate.isTrue(resources.size() <= 1); if (resources.size() == 0) { LOGGER.info(String.format("Not found resource with id %s", resourceId)); } else { resource = resources.get(0); } return resource; } @Override public Resource getResource(String resourceId, String region) { Validate.notEmpty(resourceId); Validate.notEmpty(region); StringBuilder query = new StringBuilder(); query.append(String.format("select * from %s where resourceId=? and region=?", table)); LOGGER.debug(String.format("Query is '%s'", query)); List resources = jdbcTemplate.query(query.toString(), new String[]{resourceId,region}, new RowMapper() { public Resource mapRow(ResultSet rs, int rowNum) throws SQLException { return mapResource(rs); } }); Resource resource = null; Validate.isTrue(resources.size() <= 1); if (resources.size() == 0) { LOGGER.info(String.format("Not found resource with id %s", resourceId)); } else { resource = resources.get(0); } return resource; } /** * Creates the RDS table, if it does not already exist. */ public void init() { try { LOGGER.info("Creating RDS table: {}", table); String sql = String.format("create table if not exists %s (" + " %s varchar(255), " + " %s varchar(255), " + " %s varchar(25), " + " %s varchar(255), " + " %s varchar(255), " + " %s varchar(25), " + " %s varchar(255), " + " %s BIGINT, " + " %s BIGINT, " + " %s BIGINT, " + " %s BIGINT, " + " %s BIGINT, " + " %s varchar(8), " + " %s varchar(4096) )", table, AWSResource.FIELD_RESOURCE_ID, AWSResource.FIELD_RESOURCE_TYPE, AWSResource.FIELD_REGION, AWSResource.FIELD_OWNER_EMAIL, AWSResource.FIELD_DESCRIPTION, AWSResource.FIELD_STATE, AWSResource.FIELD_TERMINATION_REASON, AWSResource.FIELD_EXPECTED_TERMINATION_TIME, AWSResource.FIELD_ACTUAL_TERMINATION_TIME, AWSResource.FIELD_NOTIFICATION_TIME, AWSResource.FIELD_LAUNCH_TIME, AWSResource.FIELD_MARK_TIME, AWSResource.FIELD_OPT_OUT_OF_JANITOR, "additionalFields"); LOGGER.debug("Create SQL is: '{}'", sql); jdbcTemplate.execute(sql); } catch (AmazonClientException e) { LOGGER.warn("Error while trying to auto-create RDS table", e); } } private HashMap additionalFieldsAsMap(Resource resource) { HashMap fields = new HashMap<>(); for(String key : resource.getAdditionalFieldNames()) { fields.put(key, resource.getAdditionalField(key)); } return fields; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/SimpleDBJanitorResourceTracker.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor; import com.amazonaws.services.simpledb.AmazonSimpleDB; import com.amazonaws.services.simpledb.model.*; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.Resource.CleanupState; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.janitor.JanitorResourceTracker; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * The JanitorResourceTracker implementation in SimpleDB. */ public class SimpleDBJanitorResourceTracker implements JanitorResourceTracker { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDBJanitorResourceTracker.class); /** The domain. */ private final String domain; /** The SimpleDB client. */ private final AmazonSimpleDB simpleDBClient; /** * Instantiates a new simple db resource tracker. * * @param awsClient * the AWS Client * @param domain * the domain */ public SimpleDBJanitorResourceTracker(AWSClient awsClient, String domain) { this.domain = domain; this.simpleDBClient = awsClient.sdbClient(); } /** * Gets the SimpleDB client. * @return the SimpleDB client */ protected AmazonSimpleDB getSimpleDBClient() { return simpleDBClient; } /** {@inheritDoc} */ @Override public void addOrUpdate(Resource resource) { List attrs = new ArrayList(); Map fieldToValueMap = resource.getFieldToValueMap(); for (Map.Entry entry : fieldToValueMap.entrySet()) { attrs.add(new ReplaceableAttribute(entry.getKey(), entry.getValue(), true)); } PutAttributesRequest putReqest = new PutAttributesRequest(domain, getSimpleDBItemName(resource), attrs); LOGGER.debug(String.format("Saving resource %s to SimpleDB domain %s", resource.getId(), domain)); this.simpleDBClient.putAttributes(putReqest); LOGGER.debug("Successfully saved."); } /** * Returns a list of AWSResource objects. You need to override this method if more * specific resource types (e.g. subtypes of AWSResource) need to be obtained from * the SimpleDB. */ @Override public List getResources(ResourceType resourceType, CleanupState state, String resourceRegion) { Validate.notEmpty(resourceRegion); List resources = new ArrayList(); StringBuilder query = new StringBuilder(); query.append(String.format("select * from `%s` where ", domain)); if (resourceType != null) { query.append(String.format("resourceType='%s' and ", resourceType)); } if (state != null) { query.append(String.format("state='%s' and ", state)); } query.append(String.format("region='%s'", resourceRegion)); LOGGER.debug(String.format("Query is '%s'", query)); List items = querySimpleDBItems(query.toString()); for (Item item : items) { try { resources.add(parseResource(item)); } catch (Exception e) { // Ignore the item that cannot be parsed. LOGGER.error(String.format("SimpleDB item %s cannot be parsed into a resource.", item)); } } LOGGER.info(String.format("Retrieved %d resources from SimpleDB in domain %s for resource type %s" + " and state %s and region %s", resources.size(), domain, resourceType, state, resourceRegion)); return resources; } @Override public Resource getResource(String resourceId) { Validate.notEmpty(resourceId); StringBuilder query = new StringBuilder(); query.append(String.format("select * from `%s` where resourceId = '%s'", domain, resourceId)); LOGGER.debug(String.format("Query is '%s'", query)); List items = querySimpleDBItems(query.toString()); Validate.isTrue(items.size() <= 1); if (items.size() == 0) { LOGGER.info(String.format("Not found resource with id %s", resourceId)); return null; } else { Resource resource = null; try { resource = parseResource(items.get(0)); } catch (Exception e) { // Ignore the item that cannot be parsed. LOGGER.error(String.format("SimpleDB item %s cannot be parsed into a resource.", items.get(0))); } return resource; } } @Override public Resource getResource(String resourceId, String region) { Validate.notEmpty(resourceId); Validate.notEmpty(region); StringBuilder query = new StringBuilder(); query.append(String.format("select * from `%s` where resourceId = '%s' and region = '%s'", domain, resourceId, region)); LOGGER.debug(String.format("Query is '%s'", query)); List items = querySimpleDBItems(query.toString()); Validate.isTrue(items.size() <= 1); if (items.size() == 0) { LOGGER.info(String.format("Not found resource with id %s and region %s", resourceId, region)); return null; } else { Resource resource = null; try { resource = parseResource(items.get(0)); } catch (Exception e) { // Ignore the item that cannot be parsed. LOGGER.error(String.format("SimpleDB item %s cannot be parsed into a resource.", items.get(0))); } return resource; } } /** * Parses a SimpleDB item into an AWS resource. * @param item the item from SimpleDB * @return the AWSResource object for the SimpleDB item */ protected Resource parseResource(Item item) { Map fieldToValue = new HashMap(); for (Attribute attr : item.getAttributes()) { String name = attr.getName(); String value = attr.getValue(); if (name != null && value != null) { fieldToValue.put(name, value); } } return AWSResource.parseFieldtoValueMap(fieldToValue); } /** * Gets the unique SimpleDB item name for a resource. The subclass can override this * method to generate the item name differently. * @param resource * @return the SimpleDB item name for the resource */ protected String getSimpleDBItemName(Resource resource) { return String.format("%s-%s-%s", resource.getResourceType().name(), resource.getId(), resource.getRegion()); } private List querySimpleDBItems(String query) { Validate.notNull(query); String nextToken = null; List items = new ArrayList(); do { SelectRequest request = new SelectRequest(query); request.setNextToken(nextToken); request.setConsistentRead(Boolean.TRUE); SelectResult result = this.simpleDBClient.select(request); items.addAll(result.getItems()); nextToken = result.getNextToken(); } while (nextToken != null); return items; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/VolumeTaggingMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import com.google.common.collect.Maps; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.Tag; import com.amazonaws.services.ec2.model.Volume; import com.amazonaws.services.ec2.model.VolumeAttachment; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.MonkeyType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.janitor.JanitorMonkey; /** * A companion monkey of Janitor Monkey for tagging EBS volumes with the last attachment information. * In many scenarios, EBS volumes generated by applications remain unattached to instances. Amazon * does not keep track of last unattached time, which makes it difficult to determine its usage. * To solve this, this monkey will tag all EBS volumes with last owner and instance to which they are attached * and the time they got detached from instance. The monkey will poll and monitor EBS volumes hourly (by default). * */ public class VolumeTaggingMonkey extends Monkey { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(VolumeTaggingMonkey.class); /** * The Interface Context. */ public interface Context extends Monkey.Context { /** * Configuration. * * @return the monkey configuration */ @Override MonkeyConfiguration configuration(); /** * AWS clients. Using a collection of clients for supporting running one monkey for multiple regions. * * @return the collection of AWS clients */ Collection awsClients(); } private final MonkeyConfiguration config; private final Collection awsClients; private final MonkeyCalendar calendar; /** We cache the global map from instance id to its owner when starting the monkey. */ private final Map> awsClientToInstanceToOwner; /** * The constructor. * @param ctx the context */ public VolumeTaggingMonkey(Context ctx) { super(ctx); this.config = ctx.configuration(); this.awsClients = ctx.awsClients(); this.calendar = ctx.calendar(); awsClientToInstanceToOwner = Maps.newHashMap(); for (AWSClient awsClient : awsClients) { Map instanceToOwner = Maps.newHashMap(); awsClientToInstanceToOwner.put(awsClient, instanceToOwner); for (Instance instance : awsClient.describeInstances()) { for (Tag tag : instance.getTags()) { if (tag.getKey().equals(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY)) { instanceToOwner.put(instance.getInstanceId(), tag.getValue()); } } } } } /** * The monkey Type. */ public enum Type implements MonkeyType { /** Volume tagging monkey. */ VOLUME_TAGGING } /** * The event types that this monkey causes. */ public enum EventTypes implements EventType { /** The event type for tagging the volume with Janitor meta data information. */ TAGGING_JANITOR } @Override public Type type() { return Type.VOLUME_TAGGING; } @Override public void doMonkeyBusiness() { String prop = "simianarmy.volumeTagging.enabled"; if (config.getBoolOrElse(prop, false)) { for (AWSClient awsClient : awsClients) { tagVolumesWithLatestAttachment(awsClient); } } else { LOGGER.info(String.format("Volume tagging monkey is not enabled. You can set %s to true to enable it.", prop)); } } private void tagVolumesWithLatestAttachment(AWSClient awsClient) { List volumes = awsClient.describeVolumes(); LOGGER.info(String.format("Trying to tag %d volumes for Janitor Monkey meta data.", volumes.size())); Date now = calendar.now().getTime(); for (Volume volume : volumes) { String owner = null, instanceId = null; Date lastDetachTime = null; List attachments = volume.getAttachments(); List tags = volume.getTags(); // The volume can have a special tag is it does not want to be changed/tagged // by Janitor monkey. if ("donotmark".equals(getTagValue(JanitorMonkey.JANITOR_TAG, tags))) { LOGGER.info(String.format("The volume %s is tagged as not handled by Janitor", volume.getVolumeId())); continue; } Map janitorMetadata = parseJanitorTag(tags); // finding the instance attached most recently. VolumeAttachment latest = null; for (VolumeAttachment attachment : attachments) { if (latest == null || latest.getAttachTime().before(attachment.getAttachTime())) { latest = attachment; } } if (latest != null) { instanceId = latest.getInstanceId(); owner = getOwnerEmail(instanceId, janitorMetadata, tags, awsClient); } if (latest == null || "detached".equals(latest.getState())) { if (janitorMetadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY) == null) { // There is no attached instance and the last detached time is not set. // Use the current time as the last detached time. LOGGER.info(String.format("Setting the last detached time to %s for volume %s", now, volume.getVolumeId())); lastDetachTime = now; } else { LOGGER.debug(String.format("The volume %s was already marked as detached at time %s", volume.getVolumeId(), janitorMetadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY))); } } else { // The volume is currently attached to an instance lastDetachTime = null; } String existingOwner = janitorMetadata.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); if (owner == null && existingOwner != null) { // Save the current owner in the tag when we are not able to find a owner. owner = existingOwner; } if (needsUpdate(janitorMetadata, owner, instanceId, lastDetachTime)) { Event evt = updateJanitorMetaTag(volume, instanceId, owner, lastDetachTime, awsClient); if (evt != null) { context().recorder().recordEvent(evt); } } } } private String getOwnerEmail(String instanceId, Map janitorMetadata, List tags, AWSClient awsClient) { // The owner of the volume is set as the owner of the last instance attached to it. String owner = awsClientToInstanceToOwner.get(awsClient).get(instanceId); if (owner == null) { owner = janitorMetadata.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } if (owner == null) { owner = getTagValue(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY, tags); } String emailDomain = getOwnerEmailDomain(); if (owner != null && !owner.contains("@") && StringUtils.isNotBlank(emailDomain)) { owner = String.format("%s@%s", owner, emailDomain); } return owner; } /** * Parses the Janitor meta tag set by this monkey and gets a map from key * to value for the tag values. * @param tags the tags of the volumes * @return the map from the Janitor meta tag key to value */ private static Map parseJanitorTag(List tags) { String janitorTag = getTagValue(JanitorMonkey.JANITOR_META_TAG, tags); return parseJanitorMetaTag(janitorTag); } /** * Parses the string of Janitor meta-data tag value to get a key value map. * @param janitorMetaTag the value of the Janitor meta-data tag * @return the key value map in the Janitor meta-data tag */ public static Map parseJanitorMetaTag(String janitorMetaTag) { Map metadata = new HashMap(); if (janitorMetaTag != null) { for (String keyValue : janitorMetaTag.split(";")) { String[] meta = keyValue.split("="); if (meta.length == 2) { metadata.put(meta[0], meta[1]); } } } return metadata; } /** Gets the domain name for the owner email. The method can be overridden in subclasses. * * @return the domain name for the owner email. */ protected String getOwnerEmailDomain() { return config.getStrOrElse("simianarmy.volumeTagging.ownerEmailDomain", ""); } private Event updateJanitorMetaTag(Volume volume, String instance, String owner, Date lastDetachTime, AWSClient awsClient) { String meta = makeMetaTag(instance, owner, lastDetachTime); Map janitorTags = new HashMap(); janitorTags.put(JanitorMonkey.JANITOR_META_TAG, meta); LOGGER.info(String.format("Setting tag %s to '%s' for volume %s", JanitorMonkey.JANITOR_META_TAG, meta, volume.getVolumeId())); String prop = "simianarmy.volumeTagging.leashed"; Event evt = null; if (config.getBoolOrElse(prop, true)) { LOGGER.info("Volume tagging monkey is leashed. No real change is made to the volume."); } else { try { awsClient.createTagsForResources(janitorTags, volume.getVolumeId()); evt = context().recorder().newEvent(type(), EventTypes.TAGGING_JANITOR, awsClient.region(), volume.getVolumeId()); evt.addField(JanitorMonkey.JANITOR_META_TAG, meta); } catch (Exception e) { LOGGER.error(String.format("Failed to update the tag for volume %s", volume.getVolumeId())); } } return evt; } /** * Makes the Janitor meta tag for volumes to track the last attachment/detachment information. * The method is intentionally made public for testing. * @param instance the last attached instance * @param owner the last owner * @param lastDetachTime the detach time * @return the meta tag of Janitor Monkey */ public static String makeMetaTag(String instance, String owner, Date lastDetachTime) { StringBuilder meta = new StringBuilder(); meta.append(String.format("%s=%s;", JanitorMonkey.INSTANCE_TAG_KEY, instance == null ? "" : instance)); meta.append(String.format("%s=%s;", BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY, owner == null ? "" : owner)); meta.append(String.format("%s=%s", JanitorMonkey.DETACH_TIME_TAG_KEY, lastDetachTime == null ? "" : AWSResource.DATE_FORMATTER.print(lastDetachTime.getTime()))); return meta.toString(); } private static String getTagValue(String key, List tags) { for (Tag tag : tags) { if (tag.getKey().equals(key)) { return tag.getValue(); } } return null; } /** Needs to update tags of the volume if * 1) owner or instance attached changed or * 2) the last detached status is changed. */ private static boolean needsUpdate(Map metadata, String owner, String instance, Date lastDetachTime) { return (owner != null && !StringUtils.equals(metadata.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY), owner)) || (instance != null && !StringUtils.equals(metadata.get(JanitorMonkey.INSTANCE_TAG_KEY), instance)) || lastDetachTime != null; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/ASGJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.Instance; import com.amazonaws.services.autoscaling.model.LaunchConfiguration; import com.amazonaws.services.autoscaling.model.SuspendedProcess; import com.amazonaws.services.autoscaling.model.TagDescription; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; /** * The crawler to crawl AWS auto scaling groups for janitor monkey. */ public class ASGJanitorCrawler extends AbstractAWSJanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ASGJanitorCrawler.class); /** The name representing the additional field name of instance ids. */ public static final String ASG_FIELD_INSTANCES = "INSTANCES"; /** The name representing the additional field name of max ASG size. */ public static final String ASG_FIELD_MAX_SIZE = "MAX_SIZE"; /** The name representing the additional field name of ELB names. */ public static final String ASG_FIELD_ELBS = "ELBS"; /** The name representing the additional field name of launch configuration name. */ public static final String ASG_FIELD_LC_NAME = "LAUNCH_CONFIGURATION_NAME"; /** The name representing the additional field name of launch configuration creation time. */ public static final String ASG_FIELD_LC_CREATION_TIME = "LAUNCH_CONFIGURATION_CREATION_TIME"; /** The name representing the additional field name of ASG suspension time from ELB. */ public static final String ASG_FIELD_SUSPENSION_TIME = "ASG_SUSPENSION_TIME"; private final Map nameToLaunchConfig = new HashMap(); /** The regular expression patter below is for the termination reason added by AWS when * an ASG is suspended from ELB's traffic. */ private static final Pattern SUSPENSION_REASON_PATTERN = Pattern.compile("User suspended at (\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}).*"); /** The date format used to print or parse the suspension time value. **/ public static final DateTimeFormatter SUSPENSION_TIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss"); /** * Instantiates a new basic ASG crawler. * @param awsClient * the aws client */ public ASGJanitorCrawler(AWSClient awsClient) { super(awsClient); } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.ASG); } @Override public List resources(ResourceType resourceType) { if ("ASG".equals(resourceType.name())) { return getASGResources(); } return Collections.emptyList(); } @Override public List resources(String... asgNames) { return getASGResources(asgNames); } private List getASGResources(String... asgNames) { AWSClient awsClient = getAWSClient(); List launchConfigurations = awsClient.describeLaunchConfigurations(); for (LaunchConfiguration lc : launchConfigurations) { nameToLaunchConfig.put(lc.getLaunchConfigurationName(), lc); } List resources = new LinkedList(); for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups(asgNames)) { Resource asgResource = new AWSResource().withId(asg.getAutoScalingGroupName()) .withResourceType(AWSResourceType.ASG).withRegion(awsClient.region()) .withLaunchTime(asg.getCreatedTime()); for (TagDescription tag : asg.getTags()) { asgResource.setTag(tag.getKey(), tag.getValue()); } asgResource.setDescription(String.format("%d instances", asg.getInstances().size())); asgResource.setOwnerEmail(getOwnerEmailForResource(asgResource)); if (asg.getStatus() != null) { ((AWSResource) asgResource).setAWSResourceState(asg.getStatus()); } Integer maxSize = asg.getMaxSize(); if (maxSize != null) { asgResource.setAdditionalField(ASG_FIELD_MAX_SIZE, String.valueOf(maxSize)); } // Adds instances and ELBs as additional fields. List instances = new ArrayList(); for (Instance instance : asg.getInstances()) { instances.add(instance.getInstanceId()); } asgResource.setAdditionalField(ASG_FIELD_INSTANCES, StringUtils.join(instances, ",")); asgResource.setAdditionalField(ASG_FIELD_ELBS, StringUtils.join(asg.getLoadBalancerNames(), ",")); String lcName = asg.getLaunchConfigurationName(); LaunchConfiguration lc = nameToLaunchConfig.get(lcName); if (lc != null) { asgResource.setAdditionalField(ASG_FIELD_LC_NAME, lcName); } if (lc != null && lc.getCreatedTime() != null) { asgResource.setAdditionalField(ASG_FIELD_LC_CREATION_TIME, String.valueOf(lc.getCreatedTime().getTime())); } // sets the field for the time when the ASG's traffic is suspended from ELB for (SuspendedProcess sp : asg.getSuspendedProcesses()) { if ("AddToLoadBalancer".equals(sp.getProcessName())) { String suspensionTime = getSuspensionTimeString(sp.getSuspensionReason()); if (suspensionTime != null) { LOGGER.info(String.format("Suspension time of ASG %s is %s", asg.getAutoScalingGroupName(), suspensionTime)); asgResource.setAdditionalField(ASG_FIELD_SUSPENSION_TIME, suspensionTime); break; } } } resources.add(asgResource); } return resources; } private String getSuspensionTimeString(String suspensionReason) { if (suspensionReason == null) { return null; } Matcher matcher = SUSPENSION_REASON_PATTERN.matcher(suspensionReason); if (matcher.matches()) { return matcher.group(1); } return null; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/AbstractAWSJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import org.apache.commons.lang.Validate; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.janitor.JanitorCrawler; /** * The abstract class for crawler of AWS resources. */ public abstract class AbstractAWSJanitorCrawler implements JanitorCrawler { /** The AWS client. */ private final AWSClient awsClient; /** * The constructor. * @param awsClient the AWS client used by the crawler. */ public AbstractAWSJanitorCrawler(AWSClient awsClient) { Validate.notNull(awsClient); this.awsClient = awsClient; } /** * Gets the owner email from the resource's tag key set in GLOBAL_OWNER_TAGKEY. * @param resource the resource * @return the owner email specified in the resource's tags */ @Override public String getOwnerEmailForResource(Resource resource) { Validate.notNull(resource); return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } /** * Gets the AWS client used by the crawler. * @return the AWS client used by the crawler. */ protected AWSClient getAWSClient() { return awsClient; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSSnapshotJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.services.ec2.model.BlockDeviceMapping; import com.amazonaws.services.ec2.model.EbsBlockDevice; import com.amazonaws.services.ec2.model.Image; import com.amazonaws.services.ec2.model.Snapshot; import com.amazonaws.services.ec2.model.Tag; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; /** * The crawler to crawl AWS EBS snapshots for janitor monkey. */ public class EBSSnapshotJanitorCrawler extends AbstractAWSJanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EBSSnapshotJanitorCrawler.class); /** The name representing the additional field name of AMIs generated using the snapshot. */ public static final String SNAPSHOT_FIELD_AMIS = "AMIs"; /** The map from snapshot id to the AMI ids that are generated using the snapshot. */ private final Map> snapshotToAMIs = new HashMap>(); /** * The constructor. * @param awsClient the AWS client */ public EBSSnapshotJanitorCrawler(AWSClient awsClient) { super(awsClient); } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.EBS_SNAPSHOT); } @Override public List resources(ResourceType resourceType) { if ("EBS_SNAPSHOT".equals(resourceType.name())) { return getSnapshotResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getSnapshotResources(resourceIds); } private List getSnapshotResources(String... snapshotIds) { refreshSnapshotToAMIs(); List resources = new LinkedList(); AWSClient awsClient = getAWSClient(); for (Snapshot snapshot : awsClient.describeSnapshots(snapshotIds)) { Resource snapshotResource = new AWSResource().withId(snapshot.getSnapshotId()) .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(snapshot.getStartTime()).withDescription(snapshot.getDescription()); for (Tag tag : snapshot.getTags()) { LOGGER.debug(String.format("Adding tag %s = %s to resource %s", tag.getKey(), tag.getValue(), snapshotResource.getId())); snapshotResource.setTag(tag.getKey(), tag.getValue()); } snapshotResource.setOwnerEmail(getOwnerEmailForResource(snapshotResource)); ((AWSResource) snapshotResource).setAWSResourceState(snapshot.getState()); Collection amis = snapshotToAMIs.get(snapshotResource.getId()); if (amis != null) { snapshotResource.setAdditionalField(SNAPSHOT_FIELD_AMIS, StringUtils.join(amis, ",")); } resources.add(snapshotResource); } return resources; } @Override public String getOwnerEmailForResource(Resource resource) { Validate.notNull(resource); String owner = resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); if (owner == null) { owner = super.getOwnerEmailForResource(resource); } return owner; } /** * Gets the collection of AMIs that are created using a specific snapshot. * @param snapshotId the snapshot id */ protected Collection getAMIsForSnapshot(String snapshotId) { Collection amis = snapshotToAMIs.get(snapshotId); if (amis != null) { return Collections.unmodifiableCollection(amis); } else { return Collections.emptyList(); } } private void refreshSnapshotToAMIs() { snapshotToAMIs.clear(); for (Image image : getAWSClient().describeImages()) { for (BlockDeviceMapping bdm : image.getBlockDeviceMappings()) { EbsBlockDevice ebd = bdm.getEbs(); if (ebd != null && ebd.getSnapshotId() != null) { LOGGER.debug(String.format("Snapshot %s is used to generate AMI %s", ebd.getSnapshotId(), image.getImageId())); Collection amis = snapshotToAMIs.get(ebd.getSnapshotId()); if (amis == null) { amis = new ArrayList(); snapshotToAMIs.put(ebd.getSnapshotId(), amis); } amis.add(image.getImageId()); } } } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/EBSVolumeJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler; import java.util.Collections; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.services.ec2.model.Tag; import com.amazonaws.services.ec2.model.Volume; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.janitor.JanitorMonkey; /** * The crawler to crawl AWS EBS volumes for janitor monkey. */ public class EBSVolumeJanitorCrawler extends AbstractAWSJanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EBSVolumeJanitorCrawler.class); /** * The constructor. * @param awsClient the AWS client */ public EBSVolumeJanitorCrawler(AWSClient awsClient) { super(awsClient); } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.EBS_VOLUME); } @Override public List resources(ResourceType resourceType) { if ("EBS_VOLUME".equals(resourceType.name())) { return getVolumeResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getVolumeResources(resourceIds); } private List getVolumeResources(String... volumeIds) { List resources = new LinkedList(); AWSClient awsClient = getAWSClient(); for (Volume volume : awsClient.describeVolumes(volumeIds)) { Resource volumeResource = new AWSResource().withId(volume.getVolumeId()) .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(volume.getCreateTime()); for (Tag tag : volume.getTags()) { LOGGER.info(String.format("Adding tag %s = %s to resource %s", tag.getKey(), tag.getValue(), volumeResource.getId())); volumeResource.setTag(tag.getKey(), tag.getValue()); } volumeResource.setOwnerEmail(getOwnerEmailForResource(volumeResource)); volumeResource.setDescription(getVolumeDescription(volume)); ((AWSResource) volumeResource).setAWSResourceState(volume.getState()); resources.add(volumeResource); } return resources; } private String getVolumeDescription(Volume volume) { StringBuilder description = new StringBuilder(); Integer size = volume.getSize(); description.append(String.format("size=%s", size == null ? "unknown" : size)); for (Tag tag : volume.getTags()) { description.append(String.format("; %s=%s", tag.getKey(), tag.getValue())); } return description.toString(); } @Override public String getOwnerEmailForResource(Resource resource) { String owner = super.getOwnerEmailForResource(resource); if (owner == null) { // try to find the owner from Janitor Metadata tag set by the volume tagging monkey. Map janitorTag = VolumeTaggingMonkey.parseJanitorMetaTag(resource.getTag( JanitorMonkey.JANITOR_META_TAG)); owner = janitorTag.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } return owner; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/ELBJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.elasticloadbalancing.model.Instance; import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription; import com.amazonaws.services.elasticloadbalancing.model.Tag; import com.amazonaws.services.elasticloadbalancing.model.TagDescription; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** * The crawler to crawl AWS instances for janitor monkey. */ public class ELBJanitorCrawler extends AbstractAWSJanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ELBJanitorCrawler.class); /** * Instantiates a new basic instance crawler. * @param awsClient * the aws client */ public ELBJanitorCrawler(AWSClient awsClient) { super(awsClient); } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.ELB); } @Override public List resources(ResourceType resourceType) { if ("ELB".equals(resourceType.name())) { return getELBResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getELBResources(resourceIds); } private List getELBResources(String... elbNames) { List resources = new LinkedList(); AWSClient awsClient = getAWSClient(); for (LoadBalancerDescription elb : awsClient.describeElasticLoadBalancers(elbNames)) { Resource resource = new AWSResource().withId(elb.getLoadBalancerName()) .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.ELB) .withLaunchTime(elb.getCreatedTime()); resource.setOwnerEmail(getOwnerEmailForResource(resource)); resources.add(resource); List instances = elb.getInstances(); if (instances == null || instances.size() == 0) { resource.setAdditionalField("instanceCount", "0"); resource.setDescription("instances=none"); LOGGER.debug(String.format("No instances found for ELB %s", resource.getId())); } else { resource.setAdditionalField("instanceCount", "" + instances.size()); ArrayList instanceList = new ArrayList(instances.size()); LOGGER.debug(String.format("Found %d instances for ELB %s", instances.size(), resource.getId())); for (Instance instance : instances) { String instanceId = instance.getInstanceId(); instanceList.add(instanceId); } String instancesStr = StringUtils.join(instanceList,","); resource.setDescription(String.format("instances=%s", instances)); LOGGER.debug(String.format("Resource ELB %s has instances %s", resource.getId(), instancesStr)); } for(TagDescription tagDescription : awsClient.describeElasticLoadBalancerTags(resource.getId())) { for(Tag tag : tagDescription.getTags()) { LOGGER.debug(String.format("Adding tag %s = %s to resource %s", tag.getKey(), tag.getValue(), resource.getId())); resource.setTag(tag.getKey(), tag.getValue()); } } } Map> elbtoASGMap = buildELBtoASGMap(); for(Resource resource : resources) { List asgList = elbtoASGMap.get(resource.getId()); if (asgList != null && asgList.size() > 0) { resource.setAdditionalField("referencedASGCount", "" + asgList.size()); String asgStr = StringUtils.join(asgList,","); resource.setDescription(resource.getDescription() + ", ASGS=" + asgStr); LOGGER.debug(String.format("Resource ELB %s is referenced by ASGs %s", resource.getId(), asgStr)); } else { resource.setAdditionalField("referencedASGCount", "0"); resource.setDescription(resource.getDescription() + ", ASGS=none"); LOGGER.debug(String.format("No ASGs found for ELB %s", resource.getId())); } } return resources; } private Map> buildELBtoASGMap() { AWSClient awsClient = getAWSClient(); LOGGER.info(String.format("Getting all ELBs associated with ASGs in region %s", awsClient.region())); List autoScalingGroupList = awsClient.describeAutoScalingGroups(); HashMap> asgMap = new HashMap<>(); for (AutoScalingGroup asg : autoScalingGroupList) { String asgName = asg.getAutoScalingGroupName(); if (asg.getLoadBalancerNames() != null ) { for (String elbName : asg.getLoadBalancerNames()) { List asgList = asgMap.get(elbName); if (asgList == null) { asgList = new ArrayList<>(); asgMap.put(elbName, asgList); } asgList.add(asgName); LOGGER.debug(String.format("Found ASG %s associated with ELB %s", asgName, elbName)); } } } return asgMap; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/InstanceJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.services.autoscaling.model.AutoScalingInstanceDetails; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.Tag; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; /** * The crawler to crawl AWS instances for janitor monkey. */ public class InstanceJanitorCrawler extends AbstractAWSJanitorCrawler { /** The name representing the additional field name of ASG's name. */ public static final String INSTANCE_FIELD_ASG_NAME = "ASG_NAME"; /** The name representing the additional field name of the OpsWork stack name. */ public static final String INSTANCE_FIELD_OPSWORKS_STACK_NAME = "OPSWORKS_STACK_NAME"; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(InstanceJanitorCrawler.class); /** * Instantiates a new basic instance crawler. * @param awsClient * the aws client */ public InstanceJanitorCrawler(AWSClient awsClient) { super(awsClient); } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.INSTANCE); } @Override public List resources(ResourceType resourceType) { if ("INSTANCE".equals(resourceType.name())) { return getInstanceResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getInstanceResources(resourceIds); } private List getInstanceResources(String... instanceIds) { List resources = new LinkedList(); AWSClient awsClient = getAWSClient(); Map idToASGInstance = new HashMap(); for (AutoScalingInstanceDetails instanceDetails : awsClient.describeAutoScalingInstances(instanceIds)) { idToASGInstance.put(instanceDetails.getInstanceId(), instanceDetails); } for (Instance instance : awsClient.describeInstances(instanceIds)) { Resource instanceResource = new AWSResource().withId(instance.getInstanceId()) .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.INSTANCE) .withLaunchTime(instance.getLaunchTime()); for (Tag tag : instance.getTags()) { instanceResource.setTag(tag.getKey(), tag.getValue()); } String description = String.format("type=%s; host=%s", instance.getInstanceType(), instance.getPublicDnsName() == null ? "" : instance.getPublicDnsName()); instanceResource.setDescription(description); instanceResource.setOwnerEmail(getOwnerEmailForResource(instanceResource)); String asgName = getAsgName(instanceResource, idToASGInstance); if (asgName != null) { instanceResource.setAdditionalField(INSTANCE_FIELD_ASG_NAME, asgName); LOGGER.info(String.format("instance %s has a ASG tag name %s.", instanceResource.getId(), asgName)); } String opsworksStackName = getOpsWorksStackName(instanceResource); if (opsworksStackName != null) { instanceResource.setAdditionalField(INSTANCE_FIELD_OPSWORKS_STACK_NAME, opsworksStackName); LOGGER.info(String.format("instance %s is part of an OpsWorks stack named %s.", instanceResource.getId(), opsworksStackName)); } if (instance.getState() != null) { ((AWSResource) instanceResource).setAWSResourceState(instance.getState().getName()); } resources.add(instanceResource); } return resources; } private String getAsgName(Resource instanceResource, Map idToASGInstance) { String asgName = instanceResource.getTag("aws:autoscaling:groupName"); if (asgName == null) { // At most times the aws:autoscaling:groupName tag has the ASG name, but there are cases // that the instance is not correctly tagged and we can find the ASG name from AutoScaling // service. AutoScalingInstanceDetails instanceDetails = idToASGInstance.get(instanceResource.getId()); if (instanceDetails != null) { asgName = instanceDetails.getAutoScalingGroupName(); } } return asgName; } private String getOpsWorksStackName(Resource instanceResource) { return instanceResource.getTag("opsworks:stack"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/LaunchConfigJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.LaunchConfiguration; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; /** * The crawler to crawl AWS launch configurations for janitor monkey. */ public class LaunchConfigJanitorCrawler extends AbstractAWSJanitorCrawler { /** The name representing the additional field name of a flag indicating if the launch config * if used by an auto scaling group. */ public static final String LAUNCH_CONFIG_FIELD_USED_BY_ASG = "USED_BY_ASG"; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(LaunchConfigJanitorCrawler.class); /** * Instantiates a new basic launch configuration crawler. * @param awsClient * the aws client */ public LaunchConfigJanitorCrawler(AWSClient awsClient) { super(awsClient); } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.LAUNCH_CONFIG); } @Override public List resources(ResourceType resourceType) { if ("LAUNCH_CONFIG".equals(resourceType.name())) { return getLaunchConfigResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getLaunchConfigResources(resourceIds); } private List getLaunchConfigResources(String... launchConfigNames) { List resources = Lists.newArrayList(); AWSClient awsClient = getAWSClient(); Set usedLCs = Sets.newHashSet(); for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups()) { usedLCs.add(asg.getLaunchConfigurationName()); } for (LaunchConfiguration launchConfiguration : awsClient.describeLaunchConfigurations(launchConfigNames)) { String lcName = launchConfiguration.getLaunchConfigurationName(); Resource lcResource = new AWSResource().withId(lcName) .withRegion(getAWSClient().region()).withResourceType(AWSResourceType.LAUNCH_CONFIG) .withLaunchTime(launchConfiguration.getCreatedTime()); lcResource.setOwnerEmail(getOwnerEmailForResource(lcResource)); lcResource.setAdditionalField(LAUNCH_CONFIG_FIELD_USED_BY_ASG, String.valueOf(usedLCs.contains(lcName))); resources.add(lcResource); } return resources; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaASGJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler.edda; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.edda.EddaClient; import com.netflix.simianarmy.janitor.JanitorCrawler; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.codehaus.jackson.JsonNode; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The crawler to crawl AWS auto scaling groups for janitor monkey using Edda. */ public class EddaASGJanitorCrawler implements JanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EddaASGJanitorCrawler.class); /** The name representing the additional field name of instance ids. */ public static final String ASG_FIELD_INSTANCES = "INSTANCES"; /** The name representing the additional field name of max ASG size. */ public static final String ASG_FIELD_MAX_SIZE = "MAX_SIZE"; /** The name representing the additional field name of ELB names. */ public static final String ASG_FIELD_ELBS = "ELBS"; /** The name representing the additional field name of launch configuration name. */ public static final String ASG_FIELD_LC_NAME = "LAUNCH_CONFIGURATION_NAME"; /** The name representing the additional field name of launch configuration creation time. */ public static final String ASG_FIELD_LC_CREATION_TIME = "LAUNCH_CONFIGURATION_CREATION_TIME"; /** The name representing the additional field name of ASG suspension time from ELB. */ public static final String ASG_FIELD_SUSPENSION_TIME = "ASG_SUSPENSION_TIME"; /** The name representing the additional field name of ASG's last change/activity time. */ public static final String ASG_FIELD_LAST_CHANGE_TIME = "ASG_LAST_CHANGE_TIME"; /** The regular expression patter below is for the termination reason added by AWS when * an ASG is suspended from ELB's traffic. */ private static final Pattern SUSPENSION_REASON_PATTERN = Pattern.compile("User suspended at (\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}).*"); private final EddaClient eddaClient; private final List regions = Lists.newArrayList(); private final Map> regionToAsgToLastChangeTime = Maps.newHashMap(); /** * Instantiates a new basic ASG crawler. * @param eddaClient * the Edda client * @param regions * the regions the crawler will crawl resources for */ public EddaASGJanitorCrawler(EddaClient eddaClient, String... regions) { Validate.notNull(eddaClient); this.eddaClient = eddaClient; Validate.notNull(regions); for (String region : regions) { this.regions.add(region); } } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.ASG); } @Override public List resources(ResourceType resourceType) { if ("ASG".equals(resourceType.name())) { return getASGResources(); } return Collections.emptyList(); } @Override public List resources(String... asgNames) { return getASGResources(asgNames); } @Override public String getOwnerEmailForResource(Resource resource) { Validate.notNull(resource); return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } private List getASGResources(String... asgNames) { refreshAsgLastChangeTime(); List resources = Lists.newArrayList(); for (String region : regions) { resources.addAll(getASGResourcesInRegion(region, asgNames)); } return resources; } private List getASGResourcesInRegion(String region, String... asgNames) { String url = eddaClient.getBaseUrl(region) + "/aws/autoScalingGroups;"; if (asgNames != null && asgNames.length != 0) { url += StringUtils.join(asgNames, ','); LOGGER.info(String.format("Getting ASGs in region %s for %d ids", region, asgNames.length)); } else { LOGGER.info(String.format("Getting all ASGs in region %s", region)); } url += ";_expand:(autoScalingGroupName,createdTime,maxSize,suspendedProcesses:(processName,suspensionReason)," + "tags:(key,value),instances:(instanceId),loadBalancerNames,launchConfigurationName)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for ASGs in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } Map lcNameToCreationTime = getLaunchConfigCreationTimes(region); List resources = Lists.newArrayList(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { resources.add(parseJsonElementToresource(region, it.next(), lcNameToCreationTime)); } return resources; } private Resource parseJsonElementToresource(String region, JsonNode jsonNode , Map lcNameToCreationTime) { Validate.notNull(jsonNode); String asgName = jsonNode.get("autoScalingGroupName").getTextValue(); long createdTime = jsonNode.get("createdTime").getLongValue(); Resource resource = new AWSResource().withId(asgName).withRegion(region) .withResourceType(AWSResourceType.ASG) .withLaunchTime(new Date(createdTime)); JsonNode tags = jsonNode.get("tags"); if (tags == null || !tags.isArray() || tags.size() == 0) { LOGGER.debug(String.format("No tags is found for %s", resource.getId())); } else { for (Iterator it = tags.getElements(); it.hasNext();) { JsonNode tag = it.next(); String key = tag.get("key").getTextValue(); String value = tag.get("value").getTextValue(); resource.setTag(key, value); } } String owner = getOwnerEmailForResource(resource); if (owner != null) { resource.setOwnerEmail(owner); } JsonNode maxSize = jsonNode.get("maxSize"); if (maxSize != null) { resource.setAdditionalField(ASG_FIELD_MAX_SIZE, String.valueOf(maxSize.getIntValue())); } // Adds instances and ELBs as additional fields. JsonNode instances = jsonNode.get("instances"); resource.setDescription(String.format("%d instances", instances.size())); List instanceIds = Lists.newArrayList(); for (Iterator it = instances.getElements(); it.hasNext();) { instanceIds.add(it.next().get("instanceId").getTextValue()); } resource.setAdditionalField(ASG_FIELD_INSTANCES, StringUtils.join(instanceIds, ",")); JsonNode elbs = jsonNode.get("loadBalancerNames"); List elbNames = Lists.newArrayList(); for (Iterator it = elbs.getElements(); it.hasNext();) { elbNames.add(it.next().getTextValue()); } resource.setAdditionalField(ASG_FIELD_ELBS, StringUtils.join(elbNames, ",")); JsonNode lc = jsonNode.get("launchConfigurationName"); if (lc != null) { String lcName = lc.getTextValue(); Long lcCreationTime = lcNameToCreationTime.get(lcName); if (lcName != null) { resource.setAdditionalField(ASG_FIELD_LC_NAME, lcName); } if (lcCreationTime != null) { resource.setAdditionalField(ASG_FIELD_LC_CREATION_TIME, String.valueOf(lcCreationTime)); } } // sets the field for the time when the ASG's traffic is suspended from ELB JsonNode suspendedProcesses = jsonNode.get("suspendedProcesses"); for (Iterator it = suspendedProcesses.getElements(); it.hasNext();) { JsonNode sp = it.next(); if ("AddToLoadBalancer".equals(sp.get("processName").getTextValue())) { String suspensionTime = getSuspensionTimeString(sp.get("suspensionReason").getTextValue()); if (suspensionTime != null) { LOGGER.info(String.format("Suspension time of ASG %s is %s", asgName, suspensionTime)); resource.setAdditionalField(ASG_FIELD_SUSPENSION_TIME, suspensionTime); break; } } } Long lastChangeTime = regionToAsgToLastChangeTime.get(region).get(asgName); if (lastChangeTime != null) { resource.setAdditionalField(ASG_FIELD_LAST_CHANGE_TIME, String.valueOf(lastChangeTime)); } return resource; } private Map getLaunchConfigCreationTimes(String region) { LOGGER.info(String.format("Getting launch configuration creation times in region %s", region)); String url = eddaClient.getBaseUrl(region) + "/aws/launchConfigurations;_expand:(launchConfigurationName,createdTime)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for lc creation times in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } Map nameToCreationTime = Maps.newHashMap(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode elem = it.next(); nameToCreationTime.put(elem.get("launchConfigurationName").getTextValue(), elem.get("createdTime").getLongValue()); } return nameToCreationTime; } private String getSuspensionTimeString(String suspensionReason) { if (suspensionReason == null) { return null; } Matcher matcher = SUSPENSION_REASON_PATTERN.matcher(suspensionReason); if (matcher.matches()) { return matcher.group(1); } return null; } private void refreshAsgLastChangeTime() { regionToAsgToLastChangeTime.clear(); for (String region : regions) { LOGGER.info(String.format("Getting ASG last change time in region %s", region)); Map asgToLastChangeTime = regionToAsgToLastChangeTime.get(region); if (asgToLastChangeTime == null) { asgToLastChangeTime = Maps.newHashMap(); regionToAsgToLastChangeTime.put(region, asgToLastChangeTime); } String url = eddaClient.getBaseUrl(region) + "/aws/autoScalingGroups;" + ";_expand;_meta:(stime,data:(autoScalingGroupName))"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for ASG last change time in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode asg = it.next(); String asgName = asg.get("data").get("autoScalingGroupName").getTextValue(); Long lastChangeTime = asg.get("stime").asLong(); LOGGER.debug(String.format("The last change time of ASG %s is %s", asgName, new DateTime(lastChangeTime))); asgToLastChangeTime.put(asgName, lastChangeTime); } } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaEBSSnapshotJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler.edda; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.edda.EddaClient; import com.netflix.simianarmy.janitor.JanitorCrawler; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.codehaus.jackson.JsonNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Map; /** * The crawler to crawl AWS EBS snapshots for janitor monkey using Edda. */ public class EddaEBSSnapshotJanitorCrawler implements JanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EddaEBSSnapshotJanitorCrawler.class); /** The name representing the additional field name of AMIs generated using the snapshot. */ public static final String SNAPSHOT_FIELD_AMIS = "AMIs"; /** The map from snapshot id to the AMI ids that are generated using the snapshot. */ private final Map> snapshotToAMIs = Maps.newHashMap(); private final EddaClient eddaClient; private final List regions = Lists.newArrayList(); private final String defaultOwnerId; /** * The constructor. * @param defaultOwnerId * the default owner id that snapshots need to have for being crawled, null means no filtering is * needed * @param eddaClient * the Edda client * @param regions * the regions the crawler will crawl resources for */ public EddaEBSSnapshotJanitorCrawler(String defaultOwnerId, EddaClient eddaClient, String... regions) { this.defaultOwnerId = defaultOwnerId; Validate.notNull(eddaClient); this.eddaClient = eddaClient; Validate.notNull(regions); for (String region : regions) { this.regions.add(region); } } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.EBS_SNAPSHOT); } @Override public List resources(ResourceType resourceType) { if ("EBS_SNAPSHOT".equals(resourceType.name())) { return getSnapshotResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getSnapshotResources(resourceIds); } private List getSnapshotResources(String... snapshotIds) { List resources = Lists.newArrayList(); for (String region : regions) { resources.addAll(getSnapshotResourcesInRegion(region, snapshotIds)); } return resources; } private List getSnapshotResourcesInRegion(String region, String... snapshotIds) { refreshSnapshotToAMIs(region); String url = eddaClient.getBaseUrl(region) + "/aws/snapshots/"; if (snapshotIds != null && snapshotIds.length != 0) { url += StringUtils.join(snapshotIds, ','); LOGGER.info(String.format("Getting snapshots in region %s for %d ids", region, snapshotIds.length)); } else { LOGGER.info(String.format("Getting all snapshots in region %s", region)); } url += ";state=completed;_expand:(snapshotId,state,description,startTime,tags,ownerId)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for snapshots in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } List resources = Lists.newArrayList(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode elem = it.next(); // Filter out shared snapshots that do not have the specified owner id. String ownerId = elem.get("ownerId").getTextValue(); if (defaultOwnerId != null && !defaultOwnerId.equals(ownerId)) { LOGGER.info(String.format("Ignoring snapshotIds %s since it does not have the specified ownerId.", elem.get("snapshotId").getTextValue())); } else { resources.add(parseJsonElementToSnapshotResource(region, elem)); } } return resources; } private Resource parseJsonElementToSnapshotResource(String region, JsonNode jsonNode) { Validate.notNull(jsonNode); long startTime = jsonNode.get("startTime").asLong(); Resource resource = new AWSResource().withId(jsonNode.get("snapshotId").getTextValue()).withRegion(region) .withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(startTime)); JsonNode tags = jsonNode.get("tags"); if (tags == null || !tags.isArray() || tags.size() == 0) { LOGGER.debug(String.format("No tags is found for %s", resource.getId())); } else { for (Iterator it = tags.getElements(); it.hasNext();) { JsonNode tag = it.next(); String key = tag.get("key").getTextValue(); String value = tag.get("value").getTextValue(); resource.setTag(key, value); } } JsonNode description = jsonNode.get("description"); if (description != null) { resource.setDescription(description.getTextValue()); } ((AWSResource) resource).setAWSResourceState(jsonNode.get("state").getTextValue()); Collection amis = snapshotToAMIs.get(resource.getId()); if (amis != null) { resource.setAdditionalField(SNAPSHOT_FIELD_AMIS, StringUtils.join(amis, ",")); } resource.setOwnerEmail(getOwnerEmailForResource(resource)); return resource; } @Override public String getOwnerEmailForResource(Resource resource) { Validate.notNull(resource); return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } /** * Gets the collection of AMIs that are created using a specific snapshot. * @param snapshotId the snapshot id */ protected Collection getAMIsForSnapshot(String snapshotId) { Collection amis = snapshotToAMIs.get(snapshotId); if (amis != null) { return Collections.unmodifiableCollection(amis); } else { return Collections.emptyList(); } } private void refreshSnapshotToAMIs(String region) { snapshotToAMIs.clear(); LOGGER.info(String.format("Getting mapping from snapshot to AMIs in region %s", region)); String url = eddaClient.getBaseUrl(region) + "/aws/images/" + ";_expand:(imageId,blockDeviceMappings:(ebs:(snapshotId)))"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for AMI mapping in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode elem = it.next(); String imageId = elem.get("imageId").getTextValue(); JsonNode blockMappings = elem.get("blockDeviceMappings"); if (blockMappings == null || !blockMappings.isArray() || blockMappings.size() == 0) { continue; } for (Iterator blockMappingsIt = blockMappings.getElements(); blockMappingsIt.hasNext();) { JsonNode blockMappingNode = blockMappingsIt.next(); JsonNode ebs = blockMappingNode.get("ebs"); if (ebs == null) { continue; } JsonNode snapshotIdNode = ebs.get("snapshotId"); String snapshotId = snapshotIdNode.getTextValue(); LOGGER.debug(String.format("Snapshot %s is used to generate AMI %s", snapshotId, imageId)); Collection amis = snapshotToAMIs.get(snapshotId); if (amis == null) { amis = Lists.newArrayList(); snapshotToAMIs.put(snapshotId, amis); } amis.add(imageId); } } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaEBSVolumeJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler.edda; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.edda.EddaClient; import com.netflix.simianarmy.janitor.JanitorCrawler; import com.netflix.simianarmy.janitor.JanitorMonkey; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.codehaus.jackson.JsonNode; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; /** * The crawler to crawl AWS EBS volumes for Janitor monkey using Edda. */ public class EddaEBSVolumeJanitorCrawler implements JanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EddaEBSVolumeJanitorCrawler.class); private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.S'Z'"); private static final int BATCH_SIZE = 50; // The value below specifies how many days we want to look back in Edda to find the owner of old instances. // In case of Edda keeps too much history data, without a reasonable date range, the query may fail. private static final int LOOKBACK_DAYS = 90; /** * The field name for purpose. */ public static final String PURPOSE = "purpose"; /** * The field name for deleteOnTermination. */ public static final String DELETE_ON_TERMINATION = "deleteOnTermination"; /** * The field name for detach time. */ public static final String DETACH_TIME = "detachTime"; private final EddaClient eddaClient; private final List regions = Lists.newArrayList(); private final Map instanceToOwner = Maps.newHashMap(); /** * The constructor. * @param eddaClient * the Edda client * @param regions * the regions the crawler will crawl resources for */ public EddaEBSVolumeJanitorCrawler(EddaClient eddaClient, String... regions) { Validate.notNull(eddaClient); this.eddaClient = eddaClient; Validate.notNull(regions); for (String region : regions) { this.regions.add(region); updateInstanceToOwner(region); } LOGGER.info(String.format("Found owner for %d instances in %s", instanceToOwner.size(), this.regions)); } private void updateInstanceToOwner(String region) { LOGGER.info(String.format("Getting owners for all instances in region %s", region)); long startTime = DateTime.now().minusDays(LOOKBACK_DAYS).getMillis(); String url = String.format("%1$s/view/instances;_since=%2$d;state.name=running;tags.key=%3$s;" + "_expand:(instanceId,tags:(key,value))", eddaClient.getBaseUrl(region), startTime, BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for instance owners in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode elem = it.next(); String instanceId = elem.get("instanceId").getTextValue(); JsonNode tags = elem.get("tags"); if (tags == null || !tags.isArray() || tags.size() == 0) { continue; } for (Iterator tagsIt = tags.getElements(); tagsIt.hasNext();) { JsonNode tag = tagsIt.next(); String tagKey = tag.get("key").getTextValue(); if (BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY.equals(tagKey)) { instanceToOwner.put(instanceId, tag.get("value").getTextValue()); break; } } } } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.EBS_VOLUME); } @Override public List resources(ResourceType resourceType) { if ("EBS_VOLUME".equals(resourceType.name())) { return getVolumeResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getVolumeResources(resourceIds); } @Override public String getOwnerEmailForResource(Resource resource) { Validate.notNull(resource); return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } private List getVolumeResources(String... volumeIds) { List resources = Lists.newArrayList(); for (String region : regions) { resources.addAll(getUnattachedVolumeResourcesInRegion(region, volumeIds)); addLastAttachmentInfo(resources); } return resources; } /** * Gets all volumes that are not attached to any instance. Janitor Monkey only considers unattached volumes * as cleanup candidates, so there is no need to get volumes that are in-use. * @param region * @return list of resources that are not attached to any instance */ private List getUnattachedVolumeResourcesInRegion(String region, String... volumeIds) { String url = eddaClient.getBaseUrl(region) + "/aws/volumes;"; if (volumeIds != null && volumeIds.length != 0) { url += StringUtils.join(volumeIds, ','); LOGGER.info(String.format("Getting volumes in region %s for %d ids", region, volumeIds.length)); } else { LOGGER.info(String.format("Getting all unattached volumes in region %s", region)); } url += ";state=available;_expand:(volumeId,createTime,size,state,tags)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for unattached volumes in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } List resources = Lists.newArrayList(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { resources.add(parseJsonElementToVolumeResource(region, it.next())); } return resources; } private Resource parseJsonElementToVolumeResource(String region, JsonNode jsonNode) { Validate.notNull(jsonNode); long createTime = jsonNode.get("createTime").asLong(); Resource resource = new AWSResource().withId(jsonNode.get("volumeId").getTextValue()).withRegion(region) .withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(createTime)); JsonNode tags = jsonNode.get("tags"); StringBuilder description = new StringBuilder(); JsonNode size = jsonNode.get("size"); description.append(String.format("size=%s", size == null ? "unknown" : size.getIntValue())); if (tags == null || !tags.isArray() || tags.size() == 0) { LOGGER.debug(String.format("No tags is found for %s", resource.getId())); } else { for (Iterator it = tags.getElements(); it.hasNext();) { JsonNode tag = it.next(); String key = tag.get("key").getTextValue(); String value = tag.get("value").getTextValue(); description.append(String.format("; %s=%s", key, value)); resource.setTag(key, value); if (key.equals(PURPOSE)) { resource.setAdditionalField(PURPOSE, value); } } resource.setDescription(description.toString()); } ((AWSResource) resource).setAWSResourceState(jsonNode.get("state").getTextValue()); return resource; } /** * Adds information of last attachment to the resources. To be compatible with the AWS implementation of * the same crawler, add the information to the JANITOR_META tag. It always uses the latest information * to update the tag in this resource (not writing back to AWS) no matter if the tag exists. * @param resources the volume resources */ private void addLastAttachmentInfo(List resources) { Validate.notNull(resources); LOGGER.info(String.format("Updating the latest attachment info for %d resources", resources.size())); Map> regionToResources = Maps.newHashMap(); for (Resource resource : resources) { List regionalList = regionToResources.get(resource.getRegion()); if (regionalList == null) { regionalList = Lists.newArrayList(); regionToResources.put(resource.getRegion(), regionalList); } regionalList.add(resource); } for (Map.Entry> entry : regionToResources.entrySet()) { LOGGER.info(String.format("Updating the latest attachment info for %d resources in region %s", resources.size(), entry.getKey())); for (List batch : Lists.partition(entry.getValue(), BATCH_SIZE)) { LOGGER.info(String.format("Processing batch of size %d", batch.size())); String batchUrl = getBatchUrl(entry.getKey(), batch); JsonNode batchResult = null; try { batchResult = eddaClient.getJsonNodeFromUrl(batchUrl); } catch (IOException e) { LOGGER.error("Failed to get response for the batch.", e); } Map idToResource = Maps.newHashMap(); for (Resource resource : batch) { idToResource.put(resource.getId(), resource); } if (batchResult == null || !batchResult.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", batchUrl, batchResult)); } Set processedIds = Sets.newHashSet(); for (Iterator it = batchResult.getElements(); it.hasNext();) { JsonNode elem = it.next(); JsonNode data = elem.get("data"); String volumeId = data.get("volumeId").getTextValue(); Resource resource = idToResource.get(volumeId); JsonNode attachments = data.get("attachments"); if (!(attachments.isArray() && attachments.size() > 0)) { continue; } JsonNode attachment = attachments.get(0); JsonNode ltime = elem.get("ltime"); if (ltime == null || ltime.isNull()) { continue; } DateTime detachTime = new DateTime(ltime.asLong()); processedIds.add(volumeId); setAttachmentInfo(volumeId, attachment, detachTime, resource); } for (Map.Entry volumeEntry : idToResource.entrySet()) { String id = volumeEntry.getKey(); if (!processedIds.contains(id)) { Resource resource = volumeEntry.getValue(); LOGGER.info(String.format("Volume %s never was attached, use createTime %s as the detachTime", id, resource.getLaunchTime())); setAttachmentInfo(id, null, new DateTime(resource.getLaunchTime().getTime()), resource); } } } } } private void setAttachmentInfo(String volumeId, JsonNode attachment, DateTime detachTime, Resource resource) { String instanceId = null; if (attachment != null) { boolean deleteOnTermination = attachment.get(DELETE_ON_TERMINATION).getBooleanValue(); if (deleteOnTermination) { LOGGER.info(String.format( "Volume %s had set the deleteOnTermination flag as true", volumeId)); } resource.setAdditionalField(DELETE_ON_TERMINATION, String.valueOf(deleteOnTermination)); instanceId = attachment.get("instanceId").getTextValue(); } // The subclass can customize the way to get the owner for a volume String owner = getOwnerEmailForResource(resource); if (owner == null && instanceId != null) { owner = instanceToOwner.get(instanceId); } resource.setOwnerEmail(owner); String metaTag = makeMetaTag(instanceId, owner, detachTime); LOGGER.info(String.format("Setting Janitor Metatag as %s for volume %s", metaTag, volumeId)); resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag); LOGGER.info(String.format("The last detach time of volume %s is %s", volumeId, detachTime)); resource.setAdditionalField(DETACH_TIME, String.valueOf(detachTime.getMillis())); } private String makeMetaTag(String instance, String owner, DateTime lastDetachTime) { StringBuilder meta = new StringBuilder(); meta.append(String.format("%s=%s;", JanitorMonkey.INSTANCE_TAG_KEY, instance == null ? "" : instance)); meta.append(String.format("%s=%s;", BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY, owner == null ? "" : owner)); meta.append(String.format("%s=%s", JanitorMonkey.DETACH_TIME_TAG_KEY, lastDetachTime == null ? "" : AWSResource.DATE_FORMATTER.print(lastDetachTime))); return meta.toString(); } private String getBatchUrl(String region, List batch) { StringBuilder batchUrl = new StringBuilder(eddaClient.getBaseUrl(region) + "/aws/volumes/"); boolean isFirst = true; for (Resource resource : batch) { if (!isFirst) { batchUrl.append(','); } else { isFirst = false; } batchUrl.append(resource.getId()); } batchUrl.append(";data.state=in-use;_since=0;_expand;_meta:" + "(ltime,data:(volumeId,attachments:(deleteOnTermination,instanceId)))"); return batchUrl.toString(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaELBJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler.edda; import com.google.common.collect.Lists; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.edda.EddaClient; import com.netflix.simianarmy.janitor.JanitorCrawler; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.codehaus.jackson.JsonNode; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** * The crawler to crawl AWS instances for janitor monkey using Edda. */ public class EddaELBJanitorCrawler implements JanitorCrawler { class DNSEntry { String dnsName; String dnsType; String hostedZoneId; }; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EddaELBJanitorCrawler.class); private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.S'Z'"); private final EddaClient eddaClient; private final List regions = Lists.newArrayList(); private final boolean useEddaApplicationOwner; private final String fallbackOwnerEmail; private Map applicationToOwner = new HashMap(); /** * Instantiates a new basic instance crawler. * @param eddaClient * the Edda client * @param regions * the regions the crawler will crawl resources for */ public EddaELBJanitorCrawler(EddaClient eddaClient, String fallbackOwnerEmail, boolean useEddaApplicationOwner, String... regions) { this.useEddaApplicationOwner = useEddaApplicationOwner; this.fallbackOwnerEmail = fallbackOwnerEmail; Validate.notNull(eddaClient); this.eddaClient = eddaClient; Validate.notNull(regions); for (String region : regions) { this.regions.add(region); } } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.ELB); } @Override public List resources(ResourceType resourceType) { if ("ELB".equals(resourceType.name())) { return getELBResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getELBResources(resourceIds); } @Override public String getOwnerEmailForResource(Resource resource) { Validate.notNull(resource); String ownerEmail = null; if (useEddaApplicationOwner) { for (String app : applicationToOwner.keySet()) { if (resource.getId().toLowerCase().startsWith(app)) { ownerEmail = applicationToOwner.get(app); break; } } } else { ownerEmail = resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } if (ownerEmail == null) { ownerEmail = fallbackOwnerEmail; } return ownerEmail; } private List getELBResources(String... instanceIds) { if (useEddaApplicationOwner) { applicationToOwner = EddaUtils.getAllApplicationOwnerEmails(eddaClient); } List resources = Lists.newArrayList(); for (String region : regions) { resources.addAll(getELBResourcesInRegion(region, instanceIds)); } return resources; } private List getELBResourcesInRegion(String region, String... elbNames) { String url = eddaClient.getBaseUrl(region) + "/aws/loadBalancers"; if (elbNames != null && elbNames.length != 0) { url += StringUtils.join(elbNames, ','); LOGGER.info(String.format("Getting ELBs in region %s for %d names", region, elbNames.length)); } else { LOGGER.info(String.format("Getting all ELBs in region %s", region)); } url += ";_expand:(loadBalancerName,createdTime,DNSName,instances,tags:(key,value))"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for ELBs in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } List resources = Lists.newArrayList(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { resources.add(parseJsonElementToELBResource(region, it.next())); } Map> elBtoASGMap = buildELBtoASGMap(region); for(Resource resource : resources) { List asgList = elBtoASGMap.get(resource.getId()); if (asgList != null && asgList.size() > 0) { resource.setAdditionalField("referencedASGCount", "" + asgList.size()); String asgStr = StringUtils.join(asgList,","); resource.setDescription(resource.getDescription() + ", ASGS=" + asgStr); LOGGER.debug(String.format("Resource ELB %s is referenced by ASGs %s", resource.getId(), asgStr)); } else { resource.setAdditionalField("referencedASGCount", "0"); resource.setDescription(resource.getDescription() + ", ASGS=none"); LOGGER.debug(String.format("No ASGs found for ELB %s", resource.getId())); } } Map> elBtoDNSMap = buildELBtoDNSMap(region); for(Resource resource : resources) { List dnsEntryList = elBtoDNSMap.get(resource.getAdditionalField("DNSName")); if (dnsEntryList != null && dnsEntryList.size() > 0) { ArrayList dnsNames = new ArrayList<>(); ArrayList dnsTypes = new ArrayList<>(); ArrayList hostedZoneIds = new ArrayList<>(); for (DNSEntry dnsEntry : dnsEntryList) { dnsNames.add(dnsEntry.dnsName); dnsTypes.add(dnsEntry.dnsType); hostedZoneIds.add(dnsEntry.hostedZoneId); } resource.setAdditionalField("referencedDNS", StringUtils.join(dnsNames,",")); resource.setAdditionalField("referencedDNSTypes", StringUtils.join(dnsTypes,",")); resource.setAdditionalField("referencedDNSZones", StringUtils.join(hostedZoneIds,",")); resource.setDescription(resource.getDescription() + ", DNS=" + resource.getAdditionalField("referencedDNS")); LOGGER.debug(String.format("Resource ELB %s is referenced by DNS %s", resource.getId(), resource.getAdditionalField("referencedDNS"))); } else { resource.setAdditionalField("referencedDNS", ""); resource.setDescription(resource.getDescription() + ", DNS=none"); LOGGER.debug(String.format("No DNS found for ELB %s", resource.getId())); } } return resources; } private Map> buildELBtoASGMap(String region) { String url = eddaClient.getBaseUrl(region) + "/aws/autoScalingGroups;_expand:(autoScalingGroupName,loadBalancerNames)"; LOGGER.info(String.format("Getting all ELBs associated with ASGs in region %s", region)); JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get JSON node from edda for ASG ELBs in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } HashMap> asgMap = new HashMap<>(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode asgNode = it.next(); String asgName = asgNode.get("autoScalingGroupName").getTextValue(); JsonNode elbs = asgNode.get("loadBalancerNames"); if (elbs == null || !elbs.isArray() || elbs.size() == 0) { continue; } else { for (Iterator elbNode = elbs.getElements(); elbNode.hasNext();) { JsonNode elb = elbNode.next(); String elbName = elb.getTextValue(); List asgList = asgMap.get(elbName); if (asgList == null) { asgList = new ArrayList<>(); asgMap.put(elbName, asgList); } asgList.add(asgName); LOGGER.debug(String.format("Found ASG %s associated with ELB %s", asgName, elbName)); } } } return asgMap; } private Resource parseJsonElementToELBResource(String region, JsonNode jsonNode) { Validate.notNull(jsonNode); String elbName = jsonNode.get("loadBalancerName").getTextValue(); long launchTime = jsonNode.get("createdTime").getLongValue(); Resource resource = new AWSResource().withId(elbName).withRegion(region) .withResourceType(AWSResourceType.ELB) .withLaunchTime(new Date(launchTime)); String dnsName = jsonNode.get("DNSName").getTextValue(); resource.setAdditionalField("DNSName", dnsName); JsonNode tags = jsonNode.get("tags"); if (tags == null || !tags.isArray() || tags.size() == 0) { LOGGER.debug(String.format("No tags is found for %s", resource.getId())); } else { for (Iterator it = tags.getElements(); it.hasNext();) { JsonNode tag = it.next(); String key = tag.get("key").getTextValue(); String value = tag.get("value").getTextValue(); resource.setTag(key, value); } } String owner = getOwnerEmailForResource(resource); if (owner != null) { resource.setOwnerEmail(owner); } LOGGER.debug(String.format("Owner of ELB Resource %s (ELB DNS: %s) is %s", resource.getId(), resource.getAdditionalField("DNSName"), resource.getOwnerEmail())); JsonNode instances = jsonNode.get("instances"); if (instances == null || !instances.isArray() || instances.size() == 0) { resource.setAdditionalField("instanceCount", "0"); resource.setDescription("instances=none"); LOGGER.debug(String.format("No instances found for ELB %s", resource.getId())); } else { resource.setAdditionalField("instanceCount", "" + instances.size()); ArrayList instanceList = new ArrayList(instances.size()); LOGGER.debug(String.format("Found %d instances for ELB %s", instances.size(), resource.getId())); for (Iterator it = instances.getElements(); it.hasNext();) { JsonNode instance = it.next(); String instanceId = instance.get("instanceId").getTextValue(); instanceList.add(instanceId); } String instancesStr = StringUtils.join(instanceList,","); resource.setDescription(String.format("instances=%s", instances)); LOGGER.debug(String.format("Resource ELB %s has instances %s", resource.getId(), instancesStr)); } return resource; } private Map> buildELBtoDNSMap(String region) { String url = eddaClient.getBaseUrl(region) + "/aws/hostedRecords;_expand:(name,type,aliasTarget,resourceRecords:(value),zone:(id))"; LOGGER.info(String.format("Getting all ELBs associated with DNSs in region %s", region)); JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get JSON node from edda for DNS ELBs in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } HashMap> dnsMap = new HashMap<>(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode dnsNode = it.next(); String dnsName = dnsNode.get("name").getTextValue(); String dnsType = dnsNode.get("type").getTextValue(); String hostedZoneId = null; JsonNode hostedZoneNode = dnsNode.get("zone"); if (hostedZoneNode != null) { JsonNode hostedZoneIdNode = hostedZoneNode.get("id"); if (hostedZoneIdNode != null) { hostedZoneId = hostedZoneIdNode.getTextValue(); } } JsonNode aliasTarget = dnsNode.get("aliasTarget"); if (aliasTarget != null) { JsonNode aliasTargetDnsNameNode = aliasTarget.get("DNSName"); if (aliasTargetDnsNameNode != null) { String aliasTargetDnsName = aliasTargetDnsNameNode.getTextValue(); if (aliasTargetDnsName != null && aliasTargetDnsName.contains(".elb.")) { DNSEntry dnsEntry = new DNSEntry(); dnsEntry.dnsName = dnsName; dnsEntry.dnsType = dnsType; dnsEntry.hostedZoneId = hostedZoneId; if (aliasTargetDnsName.endsWith(".")) { aliasTargetDnsName = aliasTargetDnsName.substring(0, aliasTargetDnsName.length()-1); } List dnsEntryList = dnsMap.get(aliasTargetDnsName); if (dnsEntryList == null) { dnsEntryList = new ArrayList<>(); dnsMap.put(aliasTargetDnsName, dnsEntryList); } dnsEntryList.add(dnsEntry); LOGGER.debug(String.format("Found DNS %s (alias) associated with ELB DNS %s, type %s, zone %s", dnsName, aliasTargetDnsName, dnsType, hostedZoneId)); } } } JsonNode records = dnsNode.get("resourceRecords"); if (records == null || !records.isArray() || records.size() == 0) { continue; } else { for (Iterator recordNode = records.getElements(); recordNode.hasNext();) { JsonNode record = recordNode.next(); String elbDNS = record.get("value").getTextValue(); if (elbDNS.contains(".elb.")) { DNSEntry dnsEntry = new DNSEntry(); dnsEntry.dnsName = dnsName; dnsEntry.dnsType = dnsType; dnsEntry.hostedZoneId = hostedZoneId; List dnsEntryList = dnsMap.get(elbDNS); if (dnsEntryList == null) { dnsEntryList = new ArrayList<>(); dnsMap.put(elbDNS, dnsEntryList); } dnsEntryList.add(dnsEntry); LOGGER.debug(String.format("Found DNS %s associated with ELB DNS %s, type %s, zone %s", dnsName, elbDNS, dnsType, hostedZoneId)); } } } } return dnsMap; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaImageJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler.edda; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.edda.EddaClient; import com.netflix.simianarmy.janitor.JanitorCrawler; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.codehaus.jackson.JsonNode; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The crawler to crawl AWS AMIs for janitor monkey using Edda. Only images that are not currently referenced * by any existing instances or launch configurations are returned. */ public class EddaImageJanitorCrawler implements JanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EddaImageJanitorCrawler.class); /** The name representing the additional field name for the last reference time by instance. */ public static final String AMI_FIELD_LAST_INSTANCE_REF_TIME = "Last_Instance_Reference_Time"; /** The name representing the additional field name for the last reference time by launch config. */ public static final String AMI_FIELD_LAST_LC_REF_TIME = "Last_Launch_Config_Reference_Time"; /** The name representing the additional field name for whether the image is a base image. **/ public static final String AMI_FIELD_BASE_IMAGE = "Base_Image"; private static final int BATCH_SIZE = 100; private final EddaClient eddaClient; private final List regions = Lists.newArrayList(); private final Set usedByInstance = Sets.newHashSet(); private final Set usedByLaunchConfig = Sets.newHashSet(); private final Set usedNames = Sets.newHashSet(); protected final Map imageIdToName = Maps.newHashMap(); private final Map imageIdToCreationTime = Maps.newHashMap(); private final Set ancestorImageIds = Sets.newHashSet(); private String ownerId; private final int daysBack; private static final String IMAGE_ID = "ami-[a-z0-9]{8}"; private static final Pattern BASE_AMI_ID_PATTERN = Pattern.compile("^.*?base_ami_id=(" + IMAGE_ID + ").*?"); private static final Pattern ANCESTOR_ID_PATTERN = Pattern.compile("^.*?ancestor_id=(" + IMAGE_ID + ").*?$"); /** * Instantiates a new basic AMI crawler. * @param eddaClient * the Edda client * @param daysBack * the number of days that the crawler checks back in history stored in Edda * @param regions * the regions the crawler will crawl resources for */ public EddaImageJanitorCrawler(EddaClient eddaClient, String ownerId, int daysBack, String... regions) { Validate.notNull(eddaClient); this.eddaClient = eddaClient; this.ownerId = ownerId; Validate.isTrue(daysBack >= 0); this.daysBack = daysBack; Validate.notNull(regions); for (String region : regions) { this.regions.add(region); } } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.IMAGE); } @Override public List resources(ResourceType resourceType) { if ("IMAGE".equals(resourceType.name())) { return getAMIResources(); } return Collections.emptyList(); } @Override public List resources(String... imageIds) { return getAMIResources(imageIds); } @Override public String getOwnerEmailForResource(Resource resource) { Validate.notNull(resource); return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } private List getAMIResources(String... imageIds) { refreshIdToNameMap(); refreshAMIsUsedByInstance(); refreshAMIsUsedByLC(); refreshIdToCreationTime(); for (String excludedId : getExcludedImageIds()) { String name = imageIdToName.get(excludedId); usedNames.add(name); } LOGGER.info(String.format("%d image names are used across the %d regions.", usedNames.size(), regions.size())); Collection excludedImageIds = getExcludedImageIds(); List resources = Lists.newArrayList(); for (String region : regions) { try { resources.addAll(getAMIResourcesInRegion(region, excludedImageIds, imageIds)); } catch (Exception e) { LOGGER.error("AMI look up failed for {} in {}", imageIds, region, e); } } return resources; } /** * The method allows users to put their own logic to exclude a set of images from being * cleaned up by Janitor Monkey. In some cases, images are not used but still need to be * kept longer. * @return a collection of image ids that need to be excluded from Janitor Monkey */ protected Collection getExcludedImageIds() { return Sets.newHashSet(); } private JsonNode getImagesInJson(String region, String... imageIds) { String url = eddaClient.getBaseUrl(region) + "/aws/images"; if (imageIds != null && imageIds.length != 0) { url += "/" + StringUtils.join(imageIds, ','); if (imageIds.length == 1) { url +=","; // Edda will return a non-array if passing exactly one imageId which will fail the crawler } LOGGER.info(String.format("Getting unreferenced AMIs in region %s for %d ids", region, imageIds.length)); } else { LOGGER.info(String.format("Getting all unreferenced AMIs in region %s", region)); if (StringUtils.isNotBlank(ownerId)) { url += ";ownerId=" + ownerId; } } url += ";_expand:(imageId,name,description,state,tags:(key,value))"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for AMIs in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } return jsonNode; } private void refreshIdToNameMap() { imageIdToName.clear(); for (String region : regions) { JsonNode jsonNode = getImagesInJson(region); for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode ami = it.next(); String imageId = ami.get("imageId").getTextValue(); String name = ami.get("name").getTextValue(); imageIdToName.put(imageId, name); } } LOGGER.info(String.format("Got mapping from image id to name for %d ids", imageIdToName.size())); } /** * AWS doesn't provide creation time for images. We use the ctime (the creation time of the image record in Edda) * to approximate the creation time of the image. */ private void refreshIdToCreationTime() { for (String region : regions) { String url = eddaClient.getBaseUrl(region) + "/aws/images"; LOGGER.info(String.format("Getting the creation time for all AMIs in region %s", region)); if (StringUtils.isNotBlank(ownerId)) { url += ";data.ownerId=" + ownerId; } url += ";_expand;_meta:(ctime,data:(imageId))"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for creation time of AMIs in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode elem = it.next(); JsonNode data = elem.get("data"); String imageId = data.get("imageId").getTextValue(); JsonNode ctimeNode = elem.get("ctime"); if (ctimeNode != null && !ctimeNode.isNull()) { long ctime = ctimeNode.asLong(); LOGGER.debug(String.format("The image record of %s was created in Edda at %s", imageId, new DateTime(ctime))); imageIdToCreationTime.put(imageId, ctime); } } } LOGGER.info(String.format("Got creation time for %d images", imageIdToCreationTime.size())); } private List getAMIResourcesInRegion(String region, Collection excludedImageIds, String... imageIds) { JsonNode jsonNode = getImagesInJson(region, imageIds); List resources = Lists.newArrayList(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode ami = it.next(); String imageId = ami.get("imageId").getTextValue(); Resource resource = parseJsonElementToresource(region, ami); String name = ami.get("name").getTextValue(); if (excludedImageIds.contains(imageId)) { LOGGER.info(String.format("Image %s is excluded from being managed by Janitor Monkey, ignore.", imageId)); continue; } if (usedByInstance.contains(imageId) || usedByLaunchConfig.contains(imageId)) { LOGGER.info(String.format("AMI %s is referenced by existing instance or launch configuration.", imageId)); } else { LOGGER.info(String.format("AMI %s is not referenced by existing instance or launch configuration.", imageId)); if (usedNames.contains(name)) { LOGGER.info(String.format("The same AMI name %s is used in another region", name)); } else { resources.add(resource); } } } long since = DateTime.now().minusDays(daysBack).getMillis(); addLastReferenceInfo(resources, since); // Mark the base AMIs that are used as the ancestor of other images for (Resource resource : resources) { if (ancestorImageIds.contains(resource.getId())) { resource.setAdditionalField(AMI_FIELD_BASE_IMAGE, "true"); } } return resources; } private Resource parseJsonElementToresource(String region, JsonNode jsonNode) { Validate.notNull(jsonNode); String imageId = jsonNode.get("imageId").getTextValue(); Resource resource = new AWSResource().withId(imageId).withRegion(region) .withResourceType(AWSResourceType.IMAGE); Long creationTime = imageIdToCreationTime.get(imageId); if (creationTime != null) { resource.setLaunchTime(new Date(creationTime)); } JsonNode tags = jsonNode.get("tags"); if (tags == null || !tags.isArray() || tags.size() == 0) { LOGGER.debug(String.format("No tags is found for %s", resource.getId())); } else { for (Iterator it = tags.getElements(); it.hasNext();) { JsonNode tag = it.next(); String key = tag.get("key").getTextValue(); String value = tag.get("value").getTextValue(); resource.setTag(key, value); } } JsonNode descNode = jsonNode.get("description"); if (descNode != null && !descNode.isNull()) { String description = descNode.getTextValue(); resource.setDescription(description); String ancestorImageId = getBaseAmiIdFromDescription(description); if (ancestorImageId != null && !ancestorImageIds.contains(ancestorImageId)) { LOGGER.info(String.format("Found base AMI id %s from description '%s'", ancestorImageId, description)); ancestorImageIds.add(ancestorImageId); } } ((AWSResource) resource).setAWSResourceState(jsonNode.get("state").getTextValue()); String owner = getOwnerEmailForResource(resource); if (owner != null) { resource.setOwnerEmail(owner); } return resource; } private void refreshAMIsUsedByInstance() { usedByInstance.clear(); for (String region : regions) { LOGGER.info(String.format("Getting AMIs used by instances in region %s", region)); String url = eddaClient.getBaseUrl(region) + "/view/instances/;_expand:(imageId)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for AMIs used by instances in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode img = it.next(); String id = img.get("imageId").getTextValue(); usedByInstance.add(id); usedNames.add(imageIdToName.get(id)); } } LOGGER.info(String.format("Found %d image ids used by instance from Edda", usedByInstance.size())); } private void refreshAMIsUsedByLC() { usedByLaunchConfig.clear(); for (String region : regions) { LOGGER.info(String.format("Getting AMIs used by launch configs in region %s", region)); String url = eddaClient.getBaseUrl(region) + "/aws/launchConfigurations;_expand:(imageId)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for AMIs used by launch configs in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode img = it.next(); String id = img.get("imageId").getTextValue(); usedByLaunchConfig.add(id); usedNames.add(imageIdToName.get(id)); } } LOGGER.info(String.format("Found %d image ids used by launch config from Edda", usedByLaunchConfig.size())); } private void addLastReferenceInfo(List resources, long since) { Validate.notNull(resources); LOGGER.info(String.format("Updating the latest reference info for %d images", resources.size())); Map> regionToResources = Maps.newHashMap(); for (Resource resource : resources) { List regionalList = regionToResources.get(resource.getRegion()); if (regionalList == null) { regionalList = Lists.newArrayList(); regionToResources.put(resource.getRegion(), regionalList); } regionalList.add(resource); } // for (Map.Entry> entry : regionToResources.entrySet()) { String region = entry.getKey(); LOGGER.info(String.format("Updating the latest reference info for %d images in region %s", resources.size(), region)); for (List batch : Lists.partition(entry.getValue(), BATCH_SIZE)) { LOGGER.info(String.format("Processing batch of size %d", batch.size())); updateReferenceTimeByInstance(region, batch, since); updateReferenceTimeByLaunchConfig(region, batch, since); } } } private void updateReferenceTimeByInstance(String region, List batch, long since) { LOGGER.info(String.format("Getting the last reference time by instance for batch of size %d", batch.size())); String batchUrl = getInstanceBatchUrl(region, batch, since); JsonNode batchResult = null; Map idToResource = Maps.newHashMap(); for (Resource resource : batch) { idToResource.put(resource.getId(), resource); } try { batchResult = eddaClient.getJsonNodeFromUrl(batchUrl); } catch (IOException e) { LOGGER.error("Failed to get response for the batch.", e); } if (batchResult == null || !batchResult.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", batchUrl, batchResult)); } for (Iterator it = batchResult.getElements(); it.hasNext();) { JsonNode elem = it.next(); JsonNode data = elem.get("data"); String imageId = data.get("imageId").getTextValue(); String instanceId = data.get("instanceId").getTextValue(); JsonNode ltimeNode = elem.get("ltime"); if (ltimeNode != null && !ltimeNode.isNull()) { long ltime = ltimeNode.asLong(); Resource ami = idToResource.get(imageId); String lastRefTimeByInstance = ami.getAdditionalField( AMI_FIELD_LAST_INSTANCE_REF_TIME); if (lastRefTimeByInstance == null || Long.parseLong(lastRefTimeByInstance) < ltime) { LOGGER.info(String.format("The last time that the image %s was referenced by instance %s is %d", imageId, instanceId, ltime)); ami.setAdditionalField(AMI_FIELD_LAST_INSTANCE_REF_TIME, String.valueOf(ltime)); } } } } private void updateReferenceTimeByLaunchConfig(String region, List batch, long since) { LOGGER.info(String.format("Getting the last reference time by launch config for batch of size %d", batch.size())); String batchUrl = getLaunchConfigBatchUrl(region, batch, since); JsonNode batchResult = null; Map idToResource = Maps.newHashMap(); for (Resource resource : batch) { idToResource.put(resource.getId(), resource); } try { batchResult = eddaClient.getJsonNodeFromUrl(batchUrl); } catch (IOException e) { LOGGER.error("Failed to get response for the batch.", e); } if (batchResult == null || !batchResult.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", batchUrl, batchResult)); } for (Iterator it = batchResult.getElements(); it.hasNext();) { JsonNode elem = it.next(); JsonNode data = elem.get("data"); String imageId = data.get("imageId").getTextValue(); String launchConfigurationName = data.get("launchConfigurationName").getTextValue(); JsonNode ltimeNode = elem.get("ltime"); if (ltimeNode != null && !ltimeNode.isNull()) { long ltime = ltimeNode.asLong(); Resource ami = idToResource.get(imageId); String lastRefTimeByLC = ami.getAdditionalField(AMI_FIELD_LAST_LC_REF_TIME); if (lastRefTimeByLC == null || Long.parseLong(lastRefTimeByLC) < ltime) { LOGGER.info(String.format( "The last time that the image %s was referenced by launch config %s is %d", imageId, launchConfigurationName, ltime)); ami.setAdditionalField(AMI_FIELD_LAST_LC_REF_TIME, String.valueOf(ltime)); } } } } private String getInstanceBatchUrl(String region, List batch, long since) { StringBuilder batchUrl = new StringBuilder(eddaClient.getBaseUrl(region) + "/view/instances/;data.imageId="); batchUrl.append(getImageIdsString(batch)); batchUrl.append(String.format(";data.state.name=terminated;_since=%d;_expand;_meta:" + "(ltime,data:(imageId,instanceId))", since)); return batchUrl.toString(); } private String getLaunchConfigBatchUrl(String region, List batch, long since) { StringBuilder batchUrl = new StringBuilder(eddaClient.getBaseUrl(region) + "/aws/launchConfigurations/;data.imageId="); batchUrl.append(getImageIdsString(batch)); batchUrl.append(String.format(";_since=%d;_expand;_meta:(ltime,data:(imageId,launchConfigurationName))", since)); return batchUrl.toString(); } private String getImageIdsString(List resources) { StringBuilder sb = new StringBuilder(); boolean isFirst = true; for (Resource resource : resources) { if (!isFirst) { sb.append(','); } else { isFirst = false; } sb.append(resource.getId()); } return sb.toString(); } private static String getBaseAmiIdFromDescription(String imageDescription) { // base_ami_id=ami-1eb75c77,base_ami_name=servicenet-roku-qadd.dc.81210.10.44 Matcher matcher = BASE_AMI_ID_PATTERN.matcher(imageDescription); if (matcher.matches()) { return matcher.group(1); } // store=ebs,ancestor_name=ebs-centosbase-x86_64-20101124,ancestor_id=ami-7b4eb912 matcher = ANCESTOR_ID_PATTERN.matcher(imageDescription); if (matcher.matches()) { return matcher.group(1); } return null; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaInstanceJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler.edda; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.edda.EddaClient; import com.netflix.simianarmy.janitor.JanitorCrawler; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.codehaus.jackson.JsonNode; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.HashSet; import java.util.HashMap; /** * The crawler to crawl AWS instances for janitor monkey using Edda. */ public class EddaInstanceJanitorCrawler implements JanitorCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EddaInstanceJanitorCrawler.class); private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.S'Z'"); private final EddaClient eddaClient; private final List regions = Lists.newArrayList(); private final Map instanceToAsg = Maps.newHashMap(); /** Max image ids per Edda Query */ private static final int MAX_IMAGE_IDS_PER_QUERY = 40; /** * Instantiates a new basic instance crawler. * @param eddaClient * the Edda client * @param regions * the regions the crawler will crawl resources for */ public EddaInstanceJanitorCrawler(EddaClient eddaClient, String... regions) { Validate.notNull(eddaClient); this.eddaClient = eddaClient; Validate.notNull(regions); for (String region : regions) { this.regions.add(region); } } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.INSTANCE); } @Override public List resources(ResourceType resourceType) { if ("INSTANCE".equals(resourceType.name())) { return getInstanceResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getInstanceResources(resourceIds); } @Override public String getOwnerEmailForResource(Resource resource) { Validate.notNull(resource); return resource.getTag(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY); } private List getInstanceResources(String... instanceIds) { List resources = Lists.newArrayList(); for (String region : regions) { resources.addAll(getInstanceResourcesInRegion(region, instanceIds)); } return resources; } private List getInstanceResourcesInRegion(String region, String... instanceIds) { refreshAsgInstances(); String url = eddaClient.getBaseUrl(region) + "/view/instances;"; if (instanceIds != null && instanceIds.length != 0) { url += StringUtils.join(instanceIds, ','); LOGGER.info(String.format("Getting instances in region %s for %d ids", region, instanceIds.length)); } else { LOGGER.info(String.format("Getting all instances in region %s", region)); } url += ";state.name=running;_expand:(instanceId,launchTime,state:(name),instanceType,imageId" + ",publicDnsName,tags:(key,value))"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for instances in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } List resources = Lists.newArrayList(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { resources.add(parseJsonElementToInstanceResource(region, it.next())); } refreshOwnerByImage(region, resources); return resources; } private Resource parseJsonElementToInstanceResource(String region, JsonNode jsonNode) { Validate.notNull(jsonNode); String instanceId = jsonNode.get("instanceId").getTextValue(); long launchTime = jsonNode.get("launchTime").getLongValue(); Resource resource = new AWSResource().withId(instanceId).withRegion(region) .withResourceType(AWSResourceType.INSTANCE) .withLaunchTime(new Date(launchTime)); JsonNode publicDnsName = jsonNode.get("publicDnsName"); String description = String.format("type=%s; host=%s", jsonNode.get("instanceType").getTextValue(), publicDnsName == null ? "" : publicDnsName.getTextValue()); resource.setDescription(description); String owner = getOwnerEmailForResource(resource); resource.setOwnerEmail(owner); JsonNode tags = jsonNode.get("tags"); String asgName = null; if (tags == null || !tags.isArray() || tags.size() == 0) { LOGGER.debug(String.format("No tags is found for %s", resource.getId())); } else { for (Iterator it = tags.getElements(); it.hasNext();) { JsonNode tag = it.next(); String key = tag.get("key").getTextValue(); String value = tag.get("value").getTextValue(); resource.setTag(key, value); if ("aws:autoscaling:groupName".equals(key)) { asgName = value; } else if (owner == null && BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY.equals(key)) { resource.setOwnerEmail(value); } } resource.setDescription(description.toString()); } // If we cannot find ASG name in tags, use the map for the ASG name if (asgName == null) { asgName = instanceToAsg.get(instanceId); if (asgName != null) { LOGGER.debug(String.format("Failed to find ASG name in tags of %s, use the ASG name %s from map", instanceId, asgName)); } } if (asgName != null) { resource.setAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME, asgName); } ((AWSResource) resource).setAWSResourceState(jsonNode.get("state").get("name").getTextValue()); String imageId = jsonNode.get("imageId").getTextValue(); resource.setAdditionalField("imageId", imageId); return resource; } private void refreshAsgInstances() { instanceToAsg.clear(); for (String region : regions) { LOGGER.info(String.format("Getting ASG instances in region %s", region)); String url = eddaClient.getBaseUrl(region) + "/aws/autoScalingGroups" + ";_expand:(autoScalingGroupName,instances:(instanceId))"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for ASGs in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode asg = it.next(); String asgName = asg.get("autoScalingGroupName").getTextValue(); JsonNode instances = asg.get("instances"); if (instances == null || instances.isNull() || !instances.isArray() || instances.size() == 0) { continue; } for (Iterator instanceIt = instances.getElements(); instanceIt.hasNext();) { JsonNode instance = instanceIt.next(); instanceToAsg.put(instance.get("instanceId").getTextValue(), asgName); } } } } private void refreshOwnerByImage(String region, List resources) { HashSet imageIds = new HashSet<>(); for (Resource resource: resources) { if (resource.getOwnerEmail() == null) { imageIds.add(resource.getAdditionalField("imageId")); } } if (imageIds.size() > 0) { HashMap imageToOwner = new HashMap<>(); String baseurl = eddaClient.getBaseUrl(region) + "/aws/images/"; Iterator itr = imageIds.iterator(); long leftToQuery = imageIds.size(); while (leftToQuery > 0) { long batchcount = leftToQuery > MAX_IMAGE_IDS_PER_QUERY ? MAX_IMAGE_IDS_PER_QUERY : leftToQuery; leftToQuery -= batchcount; ArrayList batch = new ArrayList<>(); for(int i=0;i it = imageJsonNode.getElements(); it.hasNext();) { JsonNode image = it.next(); String imageId = image.get("imageId").getTextValue(); JsonNode tags = image.get("tags"); for (Iterator tagIt = tags.getElements(); tagIt.hasNext();) { JsonNode tag = tagIt.next(); if (tag.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY) != null) { imageToOwner.put(imageId, tag.get(BasicSimianArmyContext.GLOBAL_OWNER_TAGKEY).getTextValue()); break; } } } } } if (imageToOwner.size() > 0) { for (Resource resource: resources) { if (resource.getOwnerEmail() == null && imageToOwner.get(resource.getAdditionalField("imageId")) != null) { resource.setOwnerEmail(imageToOwner.get(resource.getAdditionalField("imageId"))); LOGGER.info(String.format("Found owner %s for instance %s in AMI %s", resource.getOwnerEmail(), resource.getId(), resource.getAdditionalField("imageId"))); } } } } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaLaunchConfigJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler.edda; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.edda.EddaClient; import com.netflix.simianarmy.janitor.JanitorCrawler; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.codehaus.jackson.JsonNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Set; /** * The crawler to crawl AWS launch configurations for janitor monkey using Edda. */ public class EddaLaunchConfigJanitorCrawler implements JanitorCrawler { /** The name representing the additional field name of a flag indicating if the launch config * if used by an auto scaling group. */ public static final String LAUNCH_CONFIG_FIELD_USED_BY_ASG = "USED_BY_ASG"; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EddaLaunchConfigJanitorCrawler.class); private final EddaClient eddaClient; private final List regions = Lists.newArrayList(); /** * Instantiates a new basic launch configuration crawler. * @param eddaClient * the Edda client * @param regions * the regions the crawler will crawl resources for */ public EddaLaunchConfigJanitorCrawler(EddaClient eddaClient, String... regions) { Validate.notNull(eddaClient); this.eddaClient = eddaClient; Validate.notNull(regions); for (String region : regions) { this.regions.add(region); } } @Override public EnumSet resourceTypes() { return EnumSet.of(AWSResourceType.LAUNCH_CONFIG); } @Override public List resources(ResourceType resourceType) { if ("LAUNCH_CONFIG".equals(resourceType.name())) { return getLaunchConfigResources(); } return Collections.emptyList(); } @Override public List resources(String... resourceIds) { return getLaunchConfigResources(resourceIds); } private List getLaunchConfigResources(String... launchConfigNames) { List resources = Lists.newArrayList(); for (String region : regions) { resources.addAll(getLaunchConfigResourcesInRegion(region, launchConfigNames)); } return resources; } @Override public String getOwnerEmailForResource(Resource resource) { //Launch Configs don't have Tags return null; } private List getLaunchConfigResourcesInRegion(String region, String... launchConfigNames) { String url = eddaClient.getBaseUrl(region) + "/aws/launchConfigurations;"; if (launchConfigNames != null && launchConfigNames.length != 0) { url += StringUtils.join(launchConfigNames, ','); LOGGER.info(String.format("Getting launch configurations in region %s for %d ids", region, launchConfigNames.length)); } else { LOGGER.info(String.format("Getting all launch configurations in region %s", region)); } url += ";_expand:(launchConfigurationName,createdTime)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for instances in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } List resources = Lists.newArrayList(); Set usedLCs = getLaunchConfigsInUse(region); for (Iterator it = jsonNode.getElements(); it.hasNext();) { JsonNode launchConfiguration = it.next(); String lcName = launchConfiguration.get("launchConfigurationName").getTextValue(); Resource lcResource = new AWSResource().withId(lcName) .withRegion(region).withResourceType(AWSResourceType.LAUNCH_CONFIG) .withLaunchTime(new Date(launchConfiguration.get("createdTime").getLongValue())); lcResource.setOwnerEmail(getOwnerEmailForResource(lcResource)); lcResource.setAdditionalField(LAUNCH_CONFIG_FIELD_USED_BY_ASG, String.valueOf(usedLCs.contains(lcName))); resources.add(lcResource); } return resources; } /** * Gets the launch configs that are currently in use by at least one ASG in a region. * @param region the region * @return the set of launch config names */ private Set getLaunchConfigsInUse(String region) { LOGGER.info(String.format("Getting all launch configurations in use in region %s", region)); String url = eddaClient.getBaseUrl(region) + "/aws/autoScalingGroups;_expand:(launchConfigurationName)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (Exception e) { LOGGER.error(String.format( "Failed to get Jason node from edda for launch configs in use in region %s.", region), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("Failed to get valid document from %s, got: %s", url, jsonNode)); } Set launchConfigs = Sets.newHashSet(); for (Iterator it = jsonNode.getElements(); it.hasNext();) { launchConfigs.add(it.next().get("launchConfigurationName").getTextValue()); } return launchConfigs; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/crawler/edda/EddaUtils.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.crawler.edda; import com.netflix.simianarmy.client.edda.EddaClient; import org.codehaus.jackson.JsonNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.UnknownHostException; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * Misc common Edda Utilities */ public class EddaUtils { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(EddaUtils.class); public static Map getAllApplicationOwnerEmails(EddaClient eddaClient) { String region = "us-east-1"; LOGGER.info(String.format("Getting all application names and emails in region %s.", region)); String url = eddaClient.getBaseUrl(region) + "/netflix/applications/;_expand:(name,email)"; JsonNode jsonNode = null; try { jsonNode = eddaClient.getJsonNodeFromUrl(url); } catch (UnknownHostException e) { LOGGER.warn(String.format("Edda endpoint is not available in region %s", region)); return Collections.emptyMap(); } catch (Exception e) { throw new RuntimeException(String.format("Failed to get Json node from url: %s", url), e); } if (jsonNode == null || !jsonNode.isArray()) { throw new RuntimeException(String.format("failed to get valid document from %s, got: %s", url, jsonNode)); } Iterator it = jsonNode.getElements(); Map appToOwner = new HashMap(); while (it.hasNext()) { JsonNode node = it.next(); String appName = node.get("name").getTextValue().toLowerCase(); String owner = node.get("email").getTextValue(); if (appName != null && owner != null) { appToOwner.put(appName, owner); } } return appToOwner; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/ami/UnusedImageRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.ami; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.janitor.crawler.edda.EddaImageJanitorCrawler; import com.netflix.simianarmy.janitor.Rule; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; /** * The rule class to clean up images that are not used. */ public class UnusedImageRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(UnusedImageRule.class); private final MonkeyCalendar calendar; private final int retentionDays; private final int lastReferenceDaysThreshold; /** * Constructor. * * @param calendar * The calendar used to calculate the termination time * @param retentionDays * The number of days that the marked ASG is retained before being terminated * @param lastReferenceDaysThreshold * The number of days that the image has been not referenced that makes the ASG be * considered obsolete */ public UnusedImageRule(MonkeyCalendar calendar, int retentionDays, int lastReferenceDaysThreshold) { Validate.notNull(calendar); Validate.isTrue(retentionDays >= 0); Validate.isTrue(lastReferenceDaysThreshold >= 0); this.calendar = calendar; this.retentionDays = retentionDays; this.lastReferenceDaysThreshold = lastReferenceDaysThreshold; } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!"IMAGE".equals(resource.getResourceType().name())) { return true; } if (!"available".equals(((AWSResource) resource).getAWSResourceState())) { return true; } if ("true".equals(resource.getAdditionalField(EddaImageJanitorCrawler.AMI_FIELD_BASE_IMAGE))) { LOGGER.info(String.format("Image %s is a base image that is used to create other images", resource.getId())); return true; } long instanceRefTime = getRefTimeInMilis(resource, EddaImageJanitorCrawler.AMI_FIELD_LAST_INSTANCE_REF_TIME); long lcRefTime = getRefTimeInMilis(resource, EddaImageJanitorCrawler.AMI_FIELD_LAST_LC_REF_TIME); Date now = calendar.now().getTime(); long windowStart = new DateTime(now.getTime()).minusDays(lastReferenceDaysThreshold).getMillis(); if (instanceRefTime < windowStart && lcRefTime < windowStart) { if (resource.getExpectedTerminationTime() == null) { Date terminationTime = calendar.getBusinessDay(now, retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(String.format("Image not referenced for %d days", lastReferenceDaysThreshold + retentionDays)); LOGGER.info(String.format( "Image %s in region %s is marked to be cleaned at %s as it is not referenced" + "for more than %d days", resource.getId(), resource.getRegion(), resource.getExpectedTerminationTime(), lastReferenceDaysThreshold)); } else { LOGGER.info(String.format("Resource %s is already marked.", resource.getId())); } return false; } return true; } /** * Tries to get the long value from the provided field. If the field does not exist, try to use the * creation time. If both do not exist, use the current time. */ private long getRefTimeInMilis(Resource resource, String field) { String fieldValue = resource.getAdditionalField(field); long refTime; if (fieldValue != null) { refTime = Long.parseLong(fieldValue); } else if (resource.getLaunchTime() != null) { LOGGER.info(String.format("No value in field %s is found, use the creation time %s as the ref time of %s", field, resource.getLaunchTime(), resource.getId())); refTime = resource.getLaunchTime().getTime(); } else { // When there is no creation time or ref time is found, we consider the image is referenced. LOGGER.info(String.format("Use the current time as the ref time of %s", resource.getId())); refTime = DateTime.now().getMillis(); } return refTime; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/ASGInstanceValidator.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.asg; import com.netflix.simianarmy.Resource; /** * The interface is for checking whether an ASG has any active instance. */ public interface ASGInstanceValidator { /** * Checks whether an ASG resource contains any active instances. * @param resource the ASG resource * @return true if the ASG contains any active instances, false otherwise. */ boolean hasActiveInstance(Resource resource); } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/DiscoveryASGInstanceValidator.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.asg; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.InstanceInfo.InstanceStatus; import com.netflix.discovery.DiscoveryClient; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler; /** * The class is for checking whether an ASG has any active instance using Discovery/Eureka. * If Discovery/Eureka is enabled, it uses its service to check if the instances in the ASG are * registered and up there. */ public class DiscoveryASGInstanceValidator implements ASGInstanceValidator { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryASGInstanceValidator.class); private final DiscoveryClient discoveryClient; /** * Constructor. * @param discoveryClient * the client to access the Discovery/Eureka service for checking the status of instances. */ public DiscoveryASGInstanceValidator(DiscoveryClient discoveryClient) { Validate.notNull(discoveryClient); this.discoveryClient = discoveryClient; } /** {@inheritDoc} */ @Override public boolean hasActiveInstance(Resource resource) { String instanceIds = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES); String maxSizeStr = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE); if (StringUtils.isBlank(instanceIds)) { if (maxSizeStr != null && Integer.parseInt(maxSizeStr) == 0) { // The ASG is empty when it has no instance and the max size of the ASG is 0. // If the max size is not 0, the ASG could probably be in the process of starting new instances. LOGGER.info(String.format("ASG %s is empty.", resource.getId())); return false; } else { LOGGER.info(String.format("ASG %s does not have instances but the max size is %s", resource.getId(), maxSizeStr)); return true; } } String[] instances = StringUtils.split(instanceIds, ","); LOGGER.debug(String.format("Checking if the %d instances in ASG %s are active.", instances.length, resource.getId())); for (String instanceId : instances) { if (isActiveInstance(instanceId)) { LOGGER.info(String.format("ASG %s has active instance.", resource.getId())); return true; } } LOGGER.info(String.format("ASG %s has no active instance.", resource.getId())); return false; } /** * Returns true if the instance is registered in Eureka/Discovery. * @param instanceId the instance id * @return true if the instance is active, false otherwise */ private boolean isActiveInstance(String instanceId) { Validate.notNull(instanceId); LOGGER.debug(String.format("Checking if instance %s is active", instanceId)); List instanceInfos = discoveryClient.getInstancesById(instanceId); for (InstanceInfo info : instanceInfos) { InstanceStatus status = info.getStatus(); if (status == InstanceStatus.UP || status == InstanceStatus.STARTING) { LOGGER.debug(String.format("Instance %s is active in Discovery.", instanceId)); return true; } } LOGGER.debug(String.format("Instance %s is not active in Discovery.", instanceId)); return false; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/DummyASGInstanceValidator.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.asg; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A dummy implementation of ASGInstanceValidator that considers every instance as active. */ public class DummyASGInstanceValidator implements ASGInstanceValidator { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(DummyASGInstanceValidator.class); /** {@inheritDoc} */ @Override public boolean hasActiveInstance(Resource resource) { String instanceIds = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES); String maxSizeStr = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE); if (StringUtils.isBlank(instanceIds)) { if (maxSizeStr != null && Integer.parseInt(maxSizeStr) == 0) { // The ASG is empty when it has no instance and the max size of the ASG is 0. // If the max size is not 0, the ASG could probably be in the process of starting new instances. LOGGER.info(String.format("ASG %s is empty.", resource.getId())); return false; } else { LOGGER.info(String.format("ASG %s does not have instances but the max size is %s", resource.getId(), maxSizeStr)); return true; } } String[] instances = StringUtils.split(instanceIds, ","); return instances.length > 0; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/OldEmptyASGRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.asg; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler; import com.netflix.simianarmy.aws.janitor.crawler.edda.EddaASGJanitorCrawler; import com.netflix.simianarmy.janitor.Rule; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; /** * The rule for detecting the ASGs that 1) have old launch configurations and * 2) do not have any instances or all instances are inactive in Eureka. * 3) are not fronted with any ELBs. */ public class OldEmptyASGRule implements Rule { private final MonkeyCalendar calendar; private final int retentionDays; private final int launchConfigAgeThreshold; private final Integer lastChangeDaysThreshold; private final ASGInstanceValidator instanceValidator; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(OldEmptyASGRule.class); /** * Constructor. * * @param calendar * The calendar used to calculate the termination time * @param retentionDays * The number of days that the marked ASG is retained before being terminated * @param launchConfigAgeThreshold * The number of days that the launch configuration for the ASG has been created that makes the ASG be * considered obsolete * @param instanceValidator * The instance validator to check if an instance is active */ public OldEmptyASGRule(MonkeyCalendar calendar, int launchConfigAgeThreshold, int retentionDays, ASGInstanceValidator instanceValidator) { this(calendar, launchConfigAgeThreshold, null, retentionDays, instanceValidator); } /** * Constructor. * * @param calendar * The calendar used to calculate the termination time * @param retentionDays * The number of days that the marked ASG is retained before being terminated * @param launchConfigAgeThreshold * The number of days that the launch configuration for the ASG has been created that makes the ASG be * considered obsolete * @param lastChangeDaysThreshold * The number of days that the launch configuration has not been changed. An ASG is considered as a * cleanup candidate only if it has no change during the last n days. The parameter can be null. * @param instanceValidator * The instance validator to check if an instance is active */ public OldEmptyASGRule(MonkeyCalendar calendar, int launchConfigAgeThreshold, Integer lastChangeDaysThreshold, int retentionDays, ASGInstanceValidator instanceValidator) { Validate.notNull(calendar); Validate.isTrue(retentionDays >= 0); Validate.isTrue(launchConfigAgeThreshold >= 0); Validate.isTrue(lastChangeDaysThreshold == null || lastChangeDaysThreshold >= 0); Validate.notNull(instanceValidator); this.calendar = calendar; this.retentionDays = retentionDays; this.launchConfigAgeThreshold = launchConfigAgeThreshold; this.lastChangeDaysThreshold = lastChangeDaysThreshold; this.instanceValidator = instanceValidator; } /** {@inheritDoc} */ @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!"ASG".equals(resource.getResourceType().name())) { return true; } if (StringUtils.isNotEmpty(resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_ELBS))) { LOGGER.info(String.format("ASG %s has ELBs.", resource.getId())); return true; } if (instanceValidator.hasActiveInstance(resource)) { LOGGER.info(String.format("ASG %s has active instance.", resource.getId())); return true; } String lcName = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME); DateTime now = new DateTime(calendar.now().getTimeInMillis()); if (StringUtils.isEmpty(lcName)) { LOGGER.error(String.format("Failed to find launch configuration for ASG %s", resource.getId())); markResource(resource, now); return false; } String lcCreationTime = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME); if (StringUtils.isEmpty(lcCreationTime)) { LOGGER.error(String.format("Failed to find creation time for launch configuration %s", lcName)); return true; } DateTime createTime = new DateTime(Long.parseLong(lcCreationTime)); if (now.isBefore(createTime.plusDays(launchConfigAgeThreshold))) { LOGGER.info(String.format("The launch configuration %s has not been created for more than %d days", lcName, launchConfigAgeThreshold)); return true; } LOGGER.info(String.format("The launch configuration %s has been created for more than %d days", lcName, launchConfigAgeThreshold)); if (lastChangeDaysThreshold != null) { String lastChangeTimeField = resource.getAdditionalField(EddaASGJanitorCrawler.ASG_FIELD_LAST_CHANGE_TIME); if (StringUtils.isNotBlank(lastChangeTimeField)) { DateTime lastChangeTime = new DateTime(Long.parseLong(lastChangeTimeField)); if (lastChangeTime.plusDays(lastChangeDaysThreshold).isAfter(now)) { LOGGER.info(String.format("ASG %s had change during the last %d days", resource.getId(), lastChangeDaysThreshold)); return true; } } } markResource(resource, now); return false; } private void markResource(Resource resource, DateTime now) { if (resource.getExpectedTerminationTime() == null) { Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(String.format( "Launch config older than %d days. Not in Discovery. No ELB.", launchConfigAgeThreshold + retentionDays)); } else { LOGGER.info(String.format("Resource %s is already marked as cleanup candidate.", resource.getId())); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/asg/SuspendedASGRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.asg; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler; import com.netflix.simianarmy.janitor.Rule; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; /** * The rule for detecting the ASGs that 1) have old launch configurations and * 2) do not have any instances or all instances are inactive in Eureka. * 3) are not fronted with any ELBs. */ public class SuspendedASGRule implements Rule { private final MonkeyCalendar calendar; private final int retentionDays; private final int suspensionAgeThreshold; private final ASGInstanceValidator instanceValidator; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(SuspendedASGRule.class); /** * Constructor. * * @param calendar * The calendar used to calculate the termination time * @param retentionDays * The number of days that the marked ASG is retained before being terminated after * being marked * @param suspensionAgeThreshold * The number of days that the ASG has been suspended from ELB that makes the ASG be * considered a cleanup candidate * @param instanceValidator * The instance validator to check if an instance is active */ public SuspendedASGRule(MonkeyCalendar calendar, int suspensionAgeThreshold, int retentionDays, ASGInstanceValidator instanceValidator) { Validate.notNull(calendar); Validate.isTrue(retentionDays >= 0); Validate.isTrue(suspensionAgeThreshold >= 0); Validate.notNull(instanceValidator); this.calendar = calendar; this.retentionDays = retentionDays; this.suspensionAgeThreshold = suspensionAgeThreshold; this.instanceValidator = instanceValidator; } /** {@inheritDoc} */ @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!"ASG".equals(resource.getResourceType().name())) { return true; } if (instanceValidator.hasActiveInstance(resource)) { return true; } String suspensionTimeStr = resource.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME); if (!StringUtils.isEmpty(suspensionTimeStr)) { DateTime createTime = ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.parseDateTime(suspensionTimeStr); DateTime now = new DateTime(calendar.now().getTimeInMillis()); if (now.isBefore(createTime.plusDays(suspensionAgeThreshold))) { LOGGER.info(String.format("The ASG %s has not been suspended for more than %d days", resource.getId(), suspensionAgeThreshold)); return true; } LOGGER.info(String.format("The ASG %s has been suspended for more than %d days", resource.getId(), suspensionAgeThreshold)); if (resource.getExpectedTerminationTime() == null) { Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(String.format( "ASG has been disabled for more than %d days and all instances are out of service in Discovery", suspensionAgeThreshold + retentionDays)); } return false; } else { LOGGER.info(String.format("ASG %s is not suspended from ELB.", resource.getId())); return true; } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/elb/OrphanedELBRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.elb; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.janitor.Rule; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.apache.commons.lang.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; /** * The rule for checking the orphaned instances that do not belong to any ASGs and * launched for certain days. */ public class OrphanedELBRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(OrphanedELBRule.class); private static final String TERMINATION_REASON = "ELB has no instances and is not referenced by any ASG"; private final MonkeyCalendar calendar; private final int retentionDays; /** * Constructor for OrphanedELBRule. * * @param calendar * The calendar used to calculate the termination time * @param retentionDays * The number of days that the marked ASG is retained before being terminated */ public OrphanedELBRule(MonkeyCalendar calendar, int retentionDays) { Validate.notNull(calendar); Validate.isTrue(retentionDays >= 0); this.calendar = calendar; this.retentionDays = retentionDays; } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!resource.getResourceType().name().equals("ELB")) { return true; } String instanceCountStr = resource.getAdditionalField("instanceCount"); String refASGCountStr = resource.getAdditionalField("referencedASGCount"); if (StringUtils.isBlank(instanceCountStr)) { LOGGER.info(String.format("Resource %s is missing instance count, not marked as a cleanup candidate.", resource.getId())); return true; } if (StringUtils.isBlank(refASGCountStr)) { LOGGER.info(String.format("Resource %s is missing referenced ASG count, not marked as a cleanup candidate.", resource.getId())); return true; } int instanceCount = NumberUtils.toInt(instanceCountStr); int refASGCount = NumberUtils.toInt(refASGCountStr); if (instanceCount == 0 && refASGCount == 0) { LOGGER.info(String.format("Resource %s is marked as cleanup candidate with 0 instances and 0 referenced ASGs (owner: %s).", resource.getId(), resource.getOwnerEmail())); markResource(resource); return false; } else { LOGGER.info(String.format("Resource %s is not marked as cleanup candidate with %d instances and %d referenced ASGs.", resource.getId(), instanceCount, refASGCount)); return true; } } private void markResource(Resource resource) { if (resource.getExpectedTerminationTime() == null) { Date terminationTime = calendar.getBusinessDay(new Date(), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(TERMINATION_REASON); } else { LOGGER.info(String.format("Resource %s is already marked as cleanup candidate.", resource.getId())); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/generic/TagValueExclusionRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.generic; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.janitor.Rule; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; /** * A rule for excluding resources that contain the provided tags (name and value). * * If a resource contains the tag and the appropriate value, it will be excluded from any * other janitor rules and will not be cleaned. * */ public class TagValueExclusionRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(TagValueExclusionRule.class); private final Map tags; /** * Constructor for TagValueExclusionRule. * * @param tags * Set of tags and values to match for exclusion */ public TagValueExclusionRule(Map tags) { this.tags = tags; } /** * Constructor for TagValueExclusionRule. Use this constructor to pass names and values as separate args. * This is intended for convenience when specifying tag names/values in property files. * * Each tag[i] = (name[i], value[i]) * * @param names * Set of names to match for exclusion. Size of names must match size of values. * @param values * Set of values to match for exclusion. Size of names must match size of values. */ public TagValueExclusionRule(String[] names, String[] values) { tags = new HashMap(); int i = 0; for(String name : names) { tags.put(name, values[i]); i++; } } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); for (String tagName : tags.keySet()) { String resourceValue = resource.getTag(tagName); if (resourceValue != null && resourceValue.equals(tags.get(tagName))) { LOGGER.debug(String.format("The resource %s has the exclusion tag %s with value %s", resource.getId(), tagName, resourceValue)); return true; } } return false; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/generic/UntaggedRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.generic; import java.util.Date; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.janitor.Rule; /** * The rule for checking the orphaned instances that do not belong to any ASGs and * launched for certain days. */ public class UntaggedRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(UntaggedRule.class); private static final String TERMINATION_REASON = "This resource is missing the required tags"; private final MonkeyCalendar calendar; private final Set tagNames; private final int retentionDaysWithOwner; private final int retentionDaysWithoutOwner; /** * Constructor for UntaggedInstanceRule. * * @param calendar * The calendar used to calculate the termination time * @param tagNames * Set of tags that needs to be set */ public UntaggedRule(MonkeyCalendar calendar, Set tagNames, int retentionDaysWithOwner, int retentionDaysWithoutOwner) { Validate.notNull(calendar); Validate.notNull(tagNames); this.calendar = calendar; this.tagNames = tagNames; this.retentionDaysWithOwner = retentionDaysWithOwner; this.retentionDaysWithoutOwner = retentionDaysWithoutOwner; } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); for (String tagName : this.tagNames) { if (((AWSResource) resource).getTag(tagName) == null) { String terminationReason = String.format(" does not have the required tag %s", tagName); LOGGER.error(String.format("The resource %s %s", resource.getId(), terminationReason)); DateTime now = new DateTime(calendar.now().getTimeInMillis()); if (resource.getExpectedTerminationTime() == null) { int retentionDays = retentionDaysWithoutOwner; if (resource.getOwnerEmail() != null) { retentionDays = retentionDaysWithOwner; } Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(terminationReason); } return false; } else { LOGGER.debug(String.format("The resource %s has the required tag %s", resource.getId(), tagName)); } } LOGGER.info(String.format("The resource %s has all required tags", resource.getId())); return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/instance/OrphanedInstanceRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.instance; import java.util.Date; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler; import com.netflix.simianarmy.janitor.Rule; /** * The rule for checking the orphaned instances that do not belong to any ASGs and * launched for certain days. */ public class OrphanedInstanceRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(OrphanedInstanceRule.class); private static final String TERMINATION_REASON = "No ASG is associated with this instance"; private static final String ASG_OR_OPSWORKS_TERMINATION_REASON = "No ASG or OpsWorks stack is associated with this instance"; private final MonkeyCalendar calendar; private final int instanceAgeThreshold; private final int retentionDaysWithOwner; private final int retentionDaysWithoutOwner; private final boolean respectOpsWorksParentage; /** * Constructor for OrphanedInstanceRule. * * @param calendar * The calendar used to calculate the termination time * @param instanceAgeThreshold * The number of days that an instance is considered as orphaned since it is launched * @param retentionDaysWithOwner * The number of days that the orphaned instance is retained before being terminated * when the instance has an owner specified * @param retentionDaysWithoutOwner * The number of days that the orphaned instance is retained before being terminated * when the instance has no owner specified * @param respectOpsWorksParentage * If true, don't consider members of an OpsWorks stack as orphans */ public OrphanedInstanceRule(MonkeyCalendar calendar, int instanceAgeThreshold, int retentionDaysWithOwner, int retentionDaysWithoutOwner, boolean respectOpsWorksParentage) { Validate.notNull(calendar); Validate.isTrue(instanceAgeThreshold >= 0); Validate.isTrue(retentionDaysWithOwner >= 0); Validate.isTrue(retentionDaysWithoutOwner >= 0); this.calendar = calendar; this.instanceAgeThreshold = instanceAgeThreshold; this.retentionDaysWithOwner = retentionDaysWithOwner; this.retentionDaysWithoutOwner = retentionDaysWithoutOwner; this.respectOpsWorksParentage = respectOpsWorksParentage; } public OrphanedInstanceRule(MonkeyCalendar calendar, int instanceAgeThreshold, int retentionDaysWithOwner, int retentionDaysWithoutOwner) { this(calendar, instanceAgeThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner, false); } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!resource.getResourceType().name().equals("INSTANCE")) { // The rule is supposed to only work on AWS instances. If a non-instance resource // is passed to the rule, the rule simply ignores it and considers it as a valid // resource not for cleanup. return true; } String awsStatus = ((AWSResource) resource).getAWSResourceState(); if (!"running".equals(awsStatus) || "pending".equals(awsStatus)) { return true; } AWSResource instanceResource = (AWSResource) resource; String asgName = instanceResource.getAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME); String opsworkStackName = instanceResource.getAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_OPSWORKS_STACK_NAME); // If there is no ASG AND it isn't an OpsWorks stack (or OpsWorks isn't respected as a parent), we have an orphan if (StringUtils.isEmpty(asgName) && (!respectOpsWorksParentage || StringUtils.isEmpty(opsworkStackName))) { if (resource.getLaunchTime() == null) { LOGGER.error(String.format("The instance %s has no launch time.", resource.getId())); return true; } else { DateTime launchTime = new DateTime(resource.getLaunchTime().getTime()); DateTime now = new DateTime(calendar.now().getTimeInMillis()); if (now.isBefore(launchTime.plusDays(instanceAgeThreshold))) { LOGGER.info(String.format("The orphaned instance %s has not launched for more than %d days", resource.getId(), instanceAgeThreshold)); return true; } LOGGER.info(String.format("The orphaned instance %s has launched for more than %d days", resource.getId(), instanceAgeThreshold)); if (resource.getExpectedTerminationTime() == null) { int retentionDays = retentionDaysWithoutOwner; if (resource.getOwnerEmail() != null) { retentionDays = retentionDaysWithOwner; } Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason((respectOpsWorksParentage) ? ASG_OR_OPSWORKS_TERMINATION_REASON : TERMINATION_REASON); } return false; } } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/launchconfig/OldUnusedLaunchConfigRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.launchconfig; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.janitor.crawler.LaunchConfigJanitorCrawler; import com.netflix.simianarmy.janitor.Rule; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; /** * The rule for detecting the launch configurations that * 1) have been created for certain days and * 2) are not used by any auto scaling groups. */ public class OldUnusedLaunchConfigRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(OldUnusedLaunchConfigRule.class); private static final String TERMINATION_REASON = "Launch config is not used by any ASG"; private final MonkeyCalendar calendar; private final int ageThreshold; private final int retentionDays; /** * Constructor for OrphanedInstanceRule. * * @param calendar * The calendar used to calculate the termination time * @param ageThreshold * The number of days that a launch configuration is considered as a cleanup candidate * since it is created * @param retentionDays * The number of days that the unused launch configuration is retained before being terminated */ public OldUnusedLaunchConfigRule(MonkeyCalendar calendar, int ageThreshold, int retentionDays) { Validate.notNull(calendar); Validate.isTrue(ageThreshold >= 0); Validate.isTrue(retentionDays >= 0); this.calendar = calendar; this.ageThreshold = ageThreshold; this.retentionDays = retentionDays; } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!"LAUNCH_CONFIG".equals(resource.getResourceType().name())) { return true; } AWSResource lcResource = (AWSResource) resource; String usedByASG = lcResource.getAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG); if (StringUtils.isNotEmpty(usedByASG) && !Boolean.parseBoolean(usedByASG)) { if (resource.getLaunchTime() == null) { LOGGER.error(String.format("The launch config %s has no creation time.", resource.getId())); return true; } else { DateTime launchTime = new DateTime(resource.getLaunchTime().getTime()); DateTime now = new DateTime(calendar.now().getTimeInMillis()); if (now.isBefore(launchTime.plusDays(ageThreshold))) { LOGGER.info(String.format("The unused launch config %s has not been created for more than %d days", resource.getId(), ageThreshold)); return true; } LOGGER.info(String.format("The unused launch config %s has been created for more than %d days", resource.getId(), ageThreshold)); if (resource.getExpectedTerminationTime() == null) { Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(TERMINATION_REASON); } return false; } } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/snapshot/NoGeneratedAMIRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.snapshot; import java.util.Date; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.janitor.crawler.EBSSnapshotJanitorCrawler; import com.netflix.simianarmy.janitor.JanitorMonkey; import com.netflix.simianarmy.janitor.Rule; /** * The rule is for checking whether an EBS snapshot has any AMIs generated from it. * If there are no AMIs generated using the snapshot and the snapshot is created * for certain days, it is marked as a cleanup candidate by this rule. */ public class NoGeneratedAMIRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(NoGeneratedAMIRule.class); private String ownerEmailOverride = null; private static final String TERMINATION_REASON = "No AMI is generated for this snapshot"; private final MonkeyCalendar calendar; private final int ageThreshold; private final int retentionDays; /** The date format used to print or parse the user specified termination date. **/ public static final DateTimeFormatter TERMINATION_DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd"); /** * Constructor. * * @param calendar * The calendar used to calculate the termination time * @param ageThreshold * The number of days that a snapshot is considered as cleanup candidate since it is created * @param retentionDays * The number of days that the volume is retained before being terminated after being marked * as cleanup candidate */ public NoGeneratedAMIRule(MonkeyCalendar calendar, int ageThreshold, int retentionDays) { this(calendar, ageThreshold, retentionDays, null); } /** * Constructor. * * @param calendar * The calendar used to calculate the termination time * @param ageThreshold * The number of days that a snapshot is considered as cleanup candidate since it is created * @param retentionDays * The number of days that the volume is retained before being terminated after being marked * as cleanup candidate * @param ownerEmailOverride * If null, send notifications to the resource owner. * If not null, send notifications to the provided owner email address instead of the resource owner. */ public NoGeneratedAMIRule(MonkeyCalendar calendar, int ageThreshold, int retentionDays, String ownerEmailOverride) { Validate.notNull(calendar); Validate.isTrue(ageThreshold >= 0); Validate.isTrue(retentionDays >= 0); this.calendar = calendar; this.ageThreshold = ageThreshold; this.retentionDays = retentionDays; this.ownerEmailOverride = ownerEmailOverride; } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!resource.getResourceType().name().equals("EBS_SNAPSHOT")) { return true; } if (!"completed".equals(((AWSResource) resource).getAWSResourceState())) { return true; } String janitorTag = resource.getTag(JanitorMonkey.JANITOR_TAG); if (janitorTag != null) { if ("donotmark".equals(janitorTag)) { LOGGER.info(String.format("The snapshot %s is tagged as not handled by Janitor", resource.getId())); return true; } try { // Owners can tag the volume with a termination date in the "janitor" tag. Date userSpecifiedDate = new Date(TERMINATION_DATE_FORMATTER.parseDateTime(janitorTag).getMillis()); resource.setExpectedTerminationTime(userSpecifiedDate); resource.setTerminationReason(String.format("User specified termination date %s", janitorTag)); if (ownerEmailOverride != null) { resource.setOwnerEmail(ownerEmailOverride); } return false; } catch (Exception e) { LOGGER.error(String.format("The janitor tag is not a user specified date: %s", janitorTag)); } } if (hasGeneratedImage(resource)) { return true; } if (resource.getLaunchTime() == null) { LOGGER.error(String.format("Snapshot %s does not have a creation time.", resource.getId())); return true; } DateTime launchTime = new DateTime(resource.getLaunchTime().getTime()); DateTime now = new DateTime(calendar.now().getTimeInMillis()); if (launchTime.plusDays(ageThreshold).isBefore(now)) { if (ownerEmailOverride != null) { resource.setOwnerEmail(ownerEmailOverride); } if (resource.getExpectedTerminationTime() == null) { Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(TERMINATION_REASON); LOGGER.info(String.format( "Snapshot %s is marked to be cleaned at %s as there is no AMI generated using it", resource.getId(), resource.getExpectedTerminationTime())); } else { LOGGER.info(String.format("Resource %s is already marked.", resource.getId())); } return false; } return true; } /** * Gets the AMI created using the snapshot. This method can be overridden by subclasses * if they use a different way to check this. * @param resource the snapshot resource * @return true if there are AMIs that are created using the snapshot, false otherwise */ protected boolean hasGeneratedImage(Resource resource) { return StringUtils.isNotEmpty(resource.getAdditionalField(EBSSnapshotJanitorCrawler.SNAPSHOT_FIELD_AMIS)); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/volume/DeleteOnTerminationRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.volume; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.janitor.crawler.edda.EddaEBSVolumeJanitorCrawler; import com.netflix.simianarmy.janitor.JanitorMonkey; import com.netflix.simianarmy.janitor.Rule; import org.apache.commons.lang.Validate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; /** * The rule is for checking whether an EBS volume is not attached to any instance and had the * DeleteOnTermination flag set in the previous attachment. This is an error case that AWS didn't * handle. The volume should have been deleted as soon as it was detached. * * NOTE: since the information came from the history, the rule will work only if Edda is enabled * for Janitor Monkey. */ public class DeleteOnTerminationRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(DeleteOnTerminationRule.class); private final MonkeyCalendar calendar; private final int retentionDays; /** The date format used to print or parse the user specified termination date. **/ private static final DateTimeFormatter TERMINATION_DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd"); /** * The termination reason for the DeleteOnTerminationRule. */ public static final String TERMINATION_REASON = "Not attached and DeleteOnTerminate flag was set"; /** * Constructor. * * @param calendar * The calendar used to calculate the termination time * @param retentionDays * The number of days that the volume is retained before being terminated after being marked * as cleanup candidate */ public DeleteOnTerminationRule(MonkeyCalendar calendar, int retentionDays) { Validate.notNull(calendar); Validate.isTrue(retentionDays >= 0); this.calendar = calendar; this.retentionDays = retentionDays; } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!resource.getResourceType().name().equals("EBS_VOLUME")) { return true; } // The state of the volume being "available" means that it is not attached to any instance. if (!"available".equals(((AWSResource) resource).getAWSResourceState())) { return true; } String janitorTag = resource.getTag(JanitorMonkey.JANITOR_TAG); if (janitorTag != null) { if ("donotmark".equals(janitorTag)) { LOGGER.info(String.format("The volume %s is tagged as not handled by Janitor", resource.getId())); return true; } try { // Owners can tag the volume with a termination date in the "janitor" tag. Date userSpecifiedDate = new Date( TERMINATION_DATE_FORMATTER.parseDateTime(janitorTag).getMillis()); resource.setExpectedTerminationTime(userSpecifiedDate); resource.setTerminationReason(String.format("User specified termination date %s", janitorTag)); return false; } catch (Exception e) { LOGGER.error(String.format("The janitor tag is not a user specified date: %s", janitorTag)); } } if ("true".equals(resource.getAdditionalField(EddaEBSVolumeJanitorCrawler.DELETE_ON_TERMINATION))) { if (resource.getExpectedTerminationTime() == null) { Date terminationTime = calendar.getBusinessDay(calendar.now().getTime(), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(TERMINATION_REASON); LOGGER.info(String.format( "Volume %s is marked to be cleaned at %s as it is detached and DeleteOnTermination was set", resource.getId(), resource.getExpectedTerminationTime())); } else { LOGGER.info(String.format("Resource %s is already marked.", resource.getId())); } return false; } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/aws/janitor/rule/volume/OldDetachedVolumeRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws.janitor.rule.volume; import java.util.Date; import java.util.Map; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey; import com.netflix.simianarmy.janitor.JanitorMonkey; import com.netflix.simianarmy.janitor.Rule; /** * The rule is for checking whether an EBS volume is detached for more than * certain days. The rule mostly relies on tags on the volume to decide if * the volume should be marked. */ public class OldDetachedVolumeRule implements Rule { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(OldDetachedVolumeRule.class); private final MonkeyCalendar calendar; private final int detachDaysThreshold; private final int retentionDays; /** The date format used to print or parse the user specified termination date. **/ public static final DateTimeFormatter TERMINATION_DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd"); /** * Constructor. * * @param calendar * The calendar used to calculate the termination time * @param detachDaysThreshold * The number of days that a volume is considered as cleanup candidate since it is detached * @param retentionDays * The number of days that the volume is retained before being terminated after being marked * as cleanup candidate */ public OldDetachedVolumeRule(MonkeyCalendar calendar, int detachDaysThreshold, int retentionDays) { Validate.notNull(calendar); Validate.isTrue(detachDaysThreshold >= 0); Validate.isTrue(retentionDays >= 0); this.calendar = calendar; this.detachDaysThreshold = detachDaysThreshold; this.retentionDays = retentionDays; } @Override public boolean isValid(Resource resource) { Validate.notNull(resource); if (!resource.getResourceType().name().equals("EBS_VOLUME")) { return true; } if (!"available".equals(((AWSResource) resource).getAWSResourceState())) { return true; } String janitorTag = resource.getTag(JanitorMonkey.JANITOR_TAG); if (janitorTag != null) { if ("donotmark".equals(janitorTag)) { LOGGER.info(String.format("The volume %s is tagged as not handled by Janitor", resource.getId())); return true; } try { // Owners can tag the volume with a termination date in the "janitor" tag. Date userSpecifiedDate = new Date( TERMINATION_DATE_FORMATTER.parseDateTime(janitorTag).getMillis()); resource.setExpectedTerminationTime(userSpecifiedDate); resource.setTerminationReason(String.format("User specified termination date %s", janitorTag)); return false; } catch (Exception e) { LOGGER.error(String.format("The janitor tag is not a user specified date: %s", janitorTag)); } } String janitorMetaTag = resource.getTag(JanitorMonkey.JANITOR_META_TAG); if (janitorMetaTag == null) { LOGGER.info(String.format("Volume %s is not tagged with the Janitor meta information, ignore.", resource.getId())); return true; } Map metadata = VolumeTaggingMonkey.parseJanitorMetaTag(janitorMetaTag); String detachTimeTag = metadata.get(JanitorMonkey.DETACH_TIME_TAG_KEY); if (detachTimeTag == null) { return true; } DateTime detachTime = null; try { detachTime = AWSResource.DATE_FORMATTER.parseDateTime(detachTimeTag); } catch (Exception e) { LOGGER.error(String.format("Detach time in the JANITOR_META tag of %s is not in the valid format: %s", resource.getId(), detachTime)); return true; } DateTime now = new DateTime(calendar.now().getTimeInMillis()); if (detachTime != null && detachTime.plusDays(detachDaysThreshold).isBefore(now)) { if (resource.getExpectedTerminationTime() == null) { Date terminationTime = calendar.getBusinessDay(new Date(now.getMillis()), retentionDays); resource.setExpectedTerminationTime(terminationTime); resource.setTerminationReason(String.format("Volume not attached for %d days", detachDaysThreshold + retentionDays)); LOGGER.info(String.format( "Volume %s is marked to be cleaned at %s as it is detached for more than %d days", resource.getId(), resource.getExpectedTerminationTime(), detachDaysThreshold)); } else { LOGGER.info(String.format("Resource %s is already marked.", resource.getId())); } return false; } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/BasicCalendar.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic; import java.util.Calendar; import java.util.Date; import java.util.Set; import java.util.TimeZone; import java.util.TreeSet; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.MonkeyConfiguration; // CHECKSTYLE IGNORE MagicNumberCheck /** * The Class BasicCalendar. */ public class BasicCalendar implements MonkeyCalendar { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicCalendar.class); /** The open hour. */ private final int openHour; /** The close hour. */ private final int closeHour; /** The tz. */ private final TimeZone tz; /** The holidays. */ protected final Set holidays = new TreeSet(); /** The cfg. */ private MonkeyConfiguration cfg; /** * Instantiates a new basic calendar. * * @param cfg * the monkey configuration */ public BasicCalendar(MonkeyConfiguration cfg) { this.cfg = cfg; openHour = (int) cfg.getNumOrElse("simianarmy.calendar.openHour", 9); closeHour = (int) cfg.getNumOrElse("simianarmy.calendar.closeHour", 15); tz = TimeZone.getTimeZone(cfg.getStrOrElse("simianarmy.calendar.timezone", "America/Los_Angeles")); } /** * Instantiates a new basic calendar. * * @param open * the open hour * @param close * the close hour * @param timezone * the timezone */ public BasicCalendar(int open, int close, TimeZone timezone) { openHour = open; closeHour = close; tz = timezone; } /** * Instantiates a new basic calendar. * * @param open * the open hour * @param close * the close hour * @param timezone * the timezone */ public BasicCalendar(MonkeyConfiguration cfg, int open, int close, TimeZone timezone) { this.cfg = cfg; openHour = open; closeHour = close; tz = timezone; } /** {@inheritDoc} */ @Override public int openHour() { return openHour; } /** {@inheritDoc} */ @Override public int closeHour() { return closeHour; } /** {@inheritDoc} */ @Override public Calendar now() { return Calendar.getInstance(tz); } /** {@inheritDoc} */ @Override public boolean isMonkeyTime(Monkey monkey) { if (cfg != null && cfg.getStr("simianarmy.calendar.isMonkeyTime") != null) { boolean monkeyTime = cfg.getBool("simianarmy.calendar.isMonkeyTime"); if (monkeyTime) { LOGGER.debug("isMonkeyTime: Found property 'simianarmy.calendar.isMonkeyTime': " + monkeyTime + ". Time for monkey."); return monkeyTime; } else { LOGGER.debug("isMonkeyTime: Found property 'simianarmy.calendar.isMonkeyTime': " + monkeyTime + ". Continuing regular calendar check for monkey time."); } } Calendar now = now(); int dow = now.get(Calendar.DAY_OF_WEEK); if (dow == Calendar.SATURDAY || dow == Calendar.SUNDAY) { LOGGER.debug("isMonkeyTime: Happy Weekend! Not time for monkey."); return false; } int hour = now.get(Calendar.HOUR_OF_DAY); if (hour < openHour || hour > closeHour) { LOGGER.debug("isMonkeyTime: Not inside open hours. Not time for monkey."); return false; } if (isHoliday(now)) { LOGGER.debug("isMonkeyTime: Happy Holiday! Not time for monkey."); return false; } LOGGER.debug("isMonkeyTime: Time for monkey."); return true; } /** * Checks if is holiday. * * @param now * the current time * @return true, if is holiday */ protected boolean isHoliday(Calendar now) { if (!holidays.contains(now.get(Calendar.YEAR))) { loadHolidays(now.get(Calendar.YEAR)); } return holidays.contains(now.get(Calendar.DAY_OF_YEAR)); } /** * Load holidays. * * @param year * the year */ protected void loadHolidays(int year) { holidays.clear(); // these aren't all strictly holidays, but days when engineers will likely // not be in the office to respond to rampaging monkeys // new years, or closest work day holidays.add(workDayInYear(year, Calendar.JANUARY, 1)); // 3rd monday == MLK Day holidays.add(dayOfYear(year, Calendar.JANUARY, Calendar.MONDAY, 3)); // 3rd monday == Presidents Day holidays.add(dayOfYear(year, Calendar.FEBRUARY, Calendar.MONDAY, 3)); // last monday == Memorial Day holidays.add(dayOfYear(year, Calendar.MAY, Calendar.MONDAY, -1)); // 4th of July, or closest work day holidays.add(workDayInYear(year, Calendar.JULY, 4)); // first monday == Labor Day holidays.add(dayOfYear(year, Calendar.SEPTEMBER, Calendar.MONDAY, 1)); // second monday == Columbus Day holidays.add(dayOfYear(year, Calendar.OCTOBER, Calendar.MONDAY, 2)); // veterans day, Nov 11th, or closest work day holidays.add(workDayInYear(year, Calendar.NOVEMBER, 11)); // 4th thursday == Thanksgiving holidays.add(dayOfYear(year, Calendar.NOVEMBER, Calendar.THURSDAY, 4)); // 4th friday == "black friday", monkey goes shopping! holidays.add(dayOfYear(year, Calendar.NOVEMBER, Calendar.FRIDAY, 4)); // christmas eve holidays.add(dayOfYear(year, Calendar.DECEMBER, 24)); // christmas day holidays.add(dayOfYear(year, Calendar.DECEMBER, 25)); // day after christmas holidays.add(dayOfYear(year, Calendar.DECEMBER, 26)); // new years eve holidays.add(dayOfYear(year, Calendar.DECEMBER, 31)); // mark the holiday set with the year, so on Jan 1 it will automatically // recalculate the holidays for next year holidays.add(year); } /** * Day of year. * * @param year * the year * @param month * the month * @param day * the day * @return the day of the year */ protected int dayOfYear(int year, int month, int day) { Calendar holiday = now(); holiday.set(Calendar.YEAR, year); holiday.set(Calendar.MONTH, month); holiday.set(Calendar.DAY_OF_MONTH, day); return holiday.get(Calendar.DAY_OF_YEAR); } /** * Day of year. * * @param year * the year * @param month * the month * @param dayOfWeek * the day of week * @param weekInMonth * the week in month * @return the day of the year */ protected int dayOfYear(int year, int month, int dayOfWeek, int weekInMonth) { Calendar holiday = now(); holiday.set(Calendar.YEAR, year); holiday.set(Calendar.MONTH, month); holiday.set(Calendar.DAY_OF_WEEK, dayOfWeek); holiday.set(Calendar.DAY_OF_WEEK_IN_MONTH, weekInMonth); return holiday.get(Calendar.DAY_OF_YEAR); } /** * Work day in year. * * @param year * the year * @param month * the month * @param day * the day * @return the day of the year adjusted to the closest workday */ protected int workDayInYear(int year, int month, int day) { Calendar holiday = now(); holiday.set(Calendar.YEAR, year); holiday.set(Calendar.MONTH, month); holiday.set(Calendar.DAY_OF_MONTH, day); int doy = holiday.get(Calendar.DAY_OF_YEAR); int dow = holiday.get(Calendar.DAY_OF_WEEK); if (dow == Calendar.SATURDAY) { return doy - 1; // FRIDAY } if (dow == Calendar.SUNDAY) { return doy + 1; // MONDAY } return doy; } @Override public Date getBusinessDay(Date date, int n) { Validate.isTrue(n >= 0); Calendar calendar = now(); calendar.setTime(date); while (isHoliday(calendar) || isWeekend(calendar) || n-- > 0) { calendar.add(Calendar.DATE, 1); } return calendar.getTime(); } private boolean isWeekend(Calendar calendar) { int dow = calendar.get(Calendar.DAY_OF_WEEK); return dow == Calendar.SATURDAY || dow == Calendar.SUNDAY; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/BasicChaosMonkeyContext.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.basic.chaos.BasicChaosEmailNotifier; import com.netflix.simianarmy.basic.chaos.BasicChaosInstanceSelector; import com.netflix.simianarmy.chaos.ChaosCrawler; import com.netflix.simianarmy.chaos.ChaosEmailNotifier; import com.netflix.simianarmy.chaos.ChaosInstanceSelector; import com.netflix.simianarmy.chaos.ChaosMonkey; import com.netflix.simianarmy.client.aws.chaos.ASGChaosCrawler; import com.netflix.simianarmy.client.aws.chaos.FilteringChaosCrawler; import com.netflix.simianarmy.client.aws.chaos.TagPredicate; /** * The Class BasicContext. This provide the basic context needed for the Chaos Monkey to run. It will configure * the Chaos Monkey based on a simianarmy.properties file and chaos.properties. The properties file can be * overridden with -Dsimianarmy.properties=/path/to/my.properties */ public class BasicChaosMonkeyContext extends BasicSimianArmyContext implements ChaosMonkey.Context { /** The crawler. */ private ChaosCrawler crawler; /** The selector. */ private ChaosInstanceSelector selector; /** The chaos email notifier. */ private ChaosEmailNotifier chaosEmailNotifier; /** * Instantiates a new basic context. */ public BasicChaosMonkeyContext() { super("simianarmy.properties", "client.properties", "chaos.properties"); MonkeyConfiguration cfg = configuration(); String tagKey = cfg.getStrOrElse("simianarmy.chaos.ASGtag.key", ""); String tagValue = cfg.getStrOrElse("simianarmy.chaos.ASGtag.value", ""); ASGChaosCrawler chaosCrawler = new ASGChaosCrawler(awsClient()); setChaosCrawler(tagKey.isEmpty() ? chaosCrawler : new FilteringChaosCrawler(chaosCrawler, new TagPredicate(tagKey, tagValue))); setChaosInstanceSelector(new BasicChaosInstanceSelector()); AmazonSimpleEmailServiceClient sesClient = new AmazonSimpleEmailServiceClient(awsClientConfig); if (configuration().getStr("simianarmy.aws.email.region") != null) { sesClient.setRegion(Region.getRegion(Regions.fromName(configuration().getStr("simianarmy.aws.email.region")))); } setChaosEmailNotifier(new BasicChaosEmailNotifier(cfg, sesClient, null)); } /** {@inheritDoc} */ @Override public ChaosCrawler chaosCrawler() { return crawler; } /** * Sets the chaos crawler. * * @param chaosCrawler * the new chaos crawler */ protected void setChaosCrawler(ChaosCrawler chaosCrawler) { this.crawler = chaosCrawler; } /** {@inheritDoc} */ @Override public ChaosInstanceSelector chaosInstanceSelector() { return selector; } /** * Sets the chaos instance selector. * * @param chaosInstanceSelector * the new chaos instance selector */ protected void setChaosInstanceSelector(ChaosInstanceSelector chaosInstanceSelector) { this.selector = chaosInstanceSelector; } @Override public ChaosEmailNotifier chaosEmailNotifier() { return chaosEmailNotifier; } /** * Sets the chaos email notifier. * * @param notifier * the chaos email notifier */ protected void setChaosEmailNotifier(ChaosEmailNotifier notifier) { this.chaosEmailNotifier = notifier; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/BasicConfiguration.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic; import com.netflix.simianarmy.MonkeyConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Properties; /** * The Class BasicConfiguration. */ public class BasicConfiguration implements MonkeyConfiguration { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicConfiguration.class); /** The properties. */ private Properties props; /** * Instantiates a new basic configuration. * @param props * the properties */ public BasicConfiguration(Properties props) { this.props = props; } /** {@inheritDoc} */ @Override public boolean getBool(String property) { return getBoolOrElse(property, false); } /** {@inheritDoc} */ @Override public boolean getBoolOrElse(String property, boolean dflt) { String val = props.getProperty(property); if (val == null) { return dflt; } val = val.trim(); return Boolean.parseBoolean(val); } /** {@inheritDoc} */ @Override public double getNumOrElse(String property, double dflt) { String val = props.getProperty(property); double result = dflt; if (val != null && !val.isEmpty()) { try { result = Double.parseDouble(val); } catch (NumberFormatException e) { LOGGER.error("failed to parse property: " + property + "; returning default value: " + dflt, e); } } return result; } /** {@inheritDoc} */ @Override public String getStr(String property) { return getStrOrElse(property, null); } /** {@inheritDoc} */ @Override public String getStrOrElse(String property, String dflt) { String val = props.getProperty(property); return val == null ? dflt : val; } /** {@inheritDoc} */ @Override public void reload() { // BasicConfiguration is based on static properties, so reload is a no-op } @Override public void reload(String groupName) { // BasicConfiguration is based on static properties, so reload is a no-op } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/BasicMonkeyServer.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import com.netflix.simianarmy.basic.conformity.BasicConformityMonkey; import com.netflix.simianarmy.basic.conformity.BasicConformityMonkeyContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.MonkeyRunner; import com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey; import com.netflix.simianarmy.basic.janitor.BasicJanitorMonkey; import com.netflix.simianarmy.basic.janitor.BasicJanitorMonkeyContext; import com.netflix.simianarmy.basic.janitor.BasicVolumeTaggingMonkeyContext; /** * Will periodically run the configured monkeys. */ @SuppressWarnings("serial") public class BasicMonkeyServer extends HttpServlet { private static final Logger LOGGER = LoggerFactory.getLogger(BasicMonkeyServer.class); private static final MonkeyRunner RUNNER = MonkeyRunner.getInstance(); /** * Add the monkeys that will be run. */ @SuppressWarnings("unchecked") public void addMonkeysToRun() { LOGGER.info("Adding Chaos Monkey."); RUNNER.replaceMonkey(this.chaosClass, this.chaosContextClass); LOGGER.info("Adding Volume Tagging Monkey."); RUNNER.replaceMonkey(VolumeTaggingMonkey.class, BasicVolumeTaggingMonkeyContext.class); LOGGER.info("Adding Janitor Monkey."); RUNNER.replaceMonkey(BasicJanitorMonkey.class, BasicJanitorMonkeyContext.class); LOGGER.info("Adding Conformity Monkey."); RUNNER.replaceMonkey(BasicConformityMonkey.class, BasicConformityMonkeyContext.class); } /** * make the class of the client object configurable. */ @SuppressWarnings("rawtypes") private Class chaosContextClass = com.netflix.simianarmy.basic.BasicChaosMonkeyContext.class; /** * make the class of the chaos object configurable. */ @SuppressWarnings("rawtypes") private Class chaosClass = com.netflix.simianarmy.basic.chaos.BasicChaosMonkey.class; @Override public void init() throws ServletException { super.init(); configureClient(); addMonkeysToRun(); RUNNER.start(); } /** * Loads the client that is configured. * @throws ServletException * if the configured client cannot be loaded properly */ @SuppressWarnings("rawtypes") private void configureClient() throws ServletException { Properties clientConfig = loadClientConfigProperties(); Class newContextClass = loadClientClass(clientConfig, "simianarmy.client.context.class"); this.chaosContextClass = (newContextClass == null ? this.chaosContextClass : newContextClass); Class newChaosClass = loadClientClass(clientConfig, "simianarmy.client.chaos.class"); this.chaosClass = (newChaosClass == null ? this.chaosClass : newChaosClass); } @SuppressWarnings("rawtypes") private Class loadClientClass(Properties clientConfig, String key) throws ServletException { ClassLoader classLoader = BasicMonkeyServer.class.getClassLoader(); try { String clientClassName = clientConfig.getProperty(key); if (clientClassName == null || clientClassName.isEmpty()) { LOGGER.info("using standard client for " + key); return null; } Class newClass = classLoader.loadClass(clientClassName); LOGGER.info("using " + key + " loaded " + newClass.getCanonicalName()); return newClass; } catch (ClassNotFoundException e) { throw new ServletException("Could not load " + key, e); } } /** * Load the client config properties file. * * @return Properties The contents of the client config file * @throws ServletException * if the file cannot be read */ private Properties loadClientConfigProperties() throws ServletException { String propertyFileName = "client.properties"; String clientConfigFileName = System.getProperty(propertyFileName, "/" + propertyFileName); LOGGER.info("using client properties " + clientConfigFileName); InputStream input = null; Properties p = new Properties(); try { try { input = BasicMonkeyServer.class.getResourceAsStream(clientConfigFileName); p.load(input); return p; } finally { if (input != null) { input.close(); } } } catch (IOException e) { throw new ServletException("Could not load " + clientConfigFileName, e); } } @SuppressWarnings("unchecked") @Override public void destroy() { RUNNER.stop(); LOGGER.info("Stopping Chaos Monkey."); RUNNER.removeMonkey(this.chaosClass); LOGGER.info("Stopping Volume Tagging Monkey."); RUNNER.removeMonkey(VolumeTaggingMonkey.class); LOGGER.info("Stopping Janitor Monkey."); RUNNER.removeMonkey(BasicJanitorMonkey.class); LOGGER.info("Stopping Conformity Monkey."); RUNNER.removeMonkey(BasicConformityMonkey.class); super.destroy(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/BasicRecorderEvent.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.MonkeyRecorder; import com.netflix.simianarmy.MonkeyType; /** * The Class BasicRecorderEvent. */ public class BasicRecorderEvent implements MonkeyRecorder.Event { /** The monkey type. */ private MonkeyType monkeyType; /** The event type. */ private EventType eventType; /** The event id. */ private String id; /** The event region. */ private String region; /** The fields. */ private Map fields = new HashMap(); /** The event time. */ private Date date; /** * Instantiates a new basic recorder event. * * @param monkeyType * the monkey type * @param eventType * the event type * @param region * the region event occurred in * @param id * the event id */ public BasicRecorderEvent(MonkeyType monkeyType, EventType eventType, String region, String id) { this.monkeyType = monkeyType; this.eventType = eventType; this.id = id; this.region = region; this.date = new Date(); } /** * Instantiates a new basic recorder event. * * @param monkeyType * the monkey type * @param eventType * the event type * @param region * the region event occurred in * @param id * the event id * @param time * the event time */ public BasicRecorderEvent(MonkeyType monkeyType, EventType eventType, String region, String id, long time) { this.monkeyType = monkeyType; this.eventType = eventType; this.id = id; this.region = region; this.date = new Date(time); } /** {@inheritDoc} */ public String id() { return id; } /** {@inheritDoc} */ public String region() { return region; } /** {@inheritDoc} */ public Date eventTime() { return new Date(date.getTime()); } /** {@inheritDoc} */ public MonkeyType monkeyType() { return monkeyType; } /** {@inheritDoc} */ public EventType eventType() { return eventType; } /** {@inheritDoc} */ public Map fields() { return Collections.unmodifiableMap(fields); } /** {@inheritDoc} */ public String field(String name) { return fields.get(name); } /** * Adds the fields. * * @param toAdd * the fields to set * @return this so you can chain many addFields calls together */ public MonkeyRecorder.Event addFields(Map toAdd) { fields.putAll(toAdd); return this; } /** {@inheritDoc} */ public MonkeyRecorder.Event addField(String name, String value) { fields.put(name, value); return this; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/BasicScheduler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.MonkeyScheduler; /** * The Class BasicScheduler. */ public class BasicScheduler implements MonkeyScheduler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicScheduler.class); /** The futures. */ private HashMap> futures = new HashMap>(); /** The scheduler. */ private final ScheduledExecutorService scheduler; /** the frequency. */ private int frequency = 1; /** the frequencyUnit. */ private TimeUnit frequencyUnit = TimeUnit.HOURS; /** * Instantiates a new basic scheduler. */ public BasicScheduler() { scheduler = Executors.newScheduledThreadPool(1); } /** * Instantiates a new basic scheduler. * * @param freq * the frequency to run on * @param freqUnit * the unit for the freq argument * @param concurrent * the concurrent number of threads */ public BasicScheduler(int freq, TimeUnit freqUnit, int concurrent) { frequency = freq; frequencyUnit = freqUnit; scheduler = Executors.newScheduledThreadPool(concurrent); } /** {@inheritDoc} */ @Override public int frequency() { return frequency; } /** {@inheritDoc} */ @Override public TimeUnit frequencyUnit() { return frequencyUnit; } /** {@inheritDoc} */ @Override public void start(Monkey monkey, Runnable command) { long cycle = TimeUnit.MILLISECONDS.convert(frequency(), frequencyUnit()); // go back 1 cycle to see if we have any events Calendar cal = Calendar.getInstance(); cal.add(Calendar.MILLISECOND, (int) (-1 * cycle)); Date then = cal.getTime(); List events = monkey.context().recorder() .findEvents(monkey.type(), Collections.emptyMap(), then); if (events.isEmpty()) { // no events so just run now futures.put(monkey.type().name(), scheduler.scheduleWithFixedDelay(command, 0, frequency(), frequencyUnit())); } else { // we have events, so set the start time to the time left in what would have been the last cycle Date eventTime = events.get(0).eventTime(); Date now = new Date(); long init = cycle - (now.getTime() - eventTime.getTime()); LOGGER.info("Detected previous events within cycle, setting " + monkey.type().name() + " start to " + new Date(now.getTime() + init)); futures.put(monkey.type().name(), scheduler.scheduleWithFixedDelay(command, init, cycle, TimeUnit.MILLISECONDS)); } } /** {@inheritDoc} */ @Override public void stop(Monkey monkey) { if (futures.containsKey(monkey.type().name())) { futures.remove(monkey.type().name()).cancel(true); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/BasicSimianArmyContext.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic; import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; import com.netflix.simianarmy.CloudClient; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.MonkeyRecorder; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.MonkeyScheduler; import com.netflix.simianarmy.aws.RDSRecorder; import com.netflix.simianarmy.aws.STSAssumeRoleSessionCredentialsProvider; import com.netflix.simianarmy.aws.SimpleDBRecorder; import com.netflix.simianarmy.client.aws.AWSClient; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.InputStream; import java.lang.reflect.Constructor; import java.util.LinkedList; import java.util.Map.Entry; import java.util.Properties; import java.util.concurrent.TimeUnit; /** * The Class BasicSimianArmyContext. */ public class BasicSimianArmyContext implements Monkey.Context { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicSimianArmyContext.class); /** The configuration properties. */ private final Properties properties = new Properties(); /** The Constant MONKEY_THREADS. */ private static final int MONKEY_THREADS = 1; /** The scheduler. */ private MonkeyScheduler scheduler; /** The calendar. */ private MonkeyCalendar calendar; /** The config. */ private BasicConfiguration config; /** The client. */ private AWSClient client; /** The recorder. */ private MonkeyRecorder recorder; /** The reported events. */ private final LinkedList eventReport; /** The AWS credentials provider to be used. */ private AWSCredentialsProvider awsCredentialsProvider = new DefaultAWSCredentialsProviderChain(); /** If configured, the ARN of Role to be assumed. */ private final String assumeRoleArn; private final String accountName; private final String account; private final String secret; private final String region; protected ClientConfiguration awsClientConfig = new ClientConfiguration(); /* If configured, the proxy to be used when making AWS API requests */ private final String proxyHost; private final String proxyPort; private final String proxyUsername; private final String proxyPassword; /** The key name of the tag owner used to tag resources - across all Monkeys */ public static String GLOBAL_OWNER_TAGKEY; /** protected constructor as the Shell is meant to be subclassed. */ protected BasicSimianArmyContext(String... configFiles) { eventReport = new LinkedList(); // Load the config files into props following the provided order. for (String configFile : configFiles) { loadConfigurationFileIntoProperties(configFile); } LOGGER.info("The following are properties in the context."); for (Entry prop : properties.entrySet()) { Object propertyKey = prop.getKey(); if (isSafeToLog(propertyKey)) { LOGGER.info(String.format("%s = %s", propertyKey, prop.getValue())); } else { LOGGER.info(String.format("%s = (not shown here)", propertyKey)); } } config = new BasicConfiguration(properties); account = config.getStr("simianarmy.client.aws.accountKey"); secret = config.getStr("simianarmy.client.aws.secretKey"); accountName = config.getStrOrElse("simianarmy.client.aws.accountName", "Default"); String defaultRegion = "us-east-1"; Region currentRegion = Regions.getCurrentRegion(); if (currentRegion != null) { defaultRegion = currentRegion.getName(); } region = config.getStrOrElse("simianarmy.client.aws.region", defaultRegion); GLOBAL_OWNER_TAGKEY = config.getStrOrElse("simianarmy.tags.owner", "owner"); // Check for and configure optional proxy configuration proxyHost = config.getStr("simianarmy.client.aws.proxyHost"); proxyPort = config.getStr("simianarmy.client.aws.proxyPort"); proxyUsername = config.getStr("simianarmy.client.aws.proxyUser"); proxyPassword = config.getStr("simianarmy.client.aws.proxyPassword"); if ((proxyHost != null) && (proxyPort != null)) { awsClientConfig.setProxyHost(proxyHost); awsClientConfig.setProxyPort(Integer.parseInt(proxyPort)); if ((proxyUsername != null) && (proxyPassword != null)) { awsClientConfig.setProxyUsername(proxyUsername); awsClientConfig.setProxyPassword(proxyPassword); } } assumeRoleArn = config.getStr("simianarmy.client.aws.assumeRoleArn"); if (assumeRoleArn != null) { this.awsCredentialsProvider = new STSAssumeRoleSessionCredentialsProvider(assumeRoleArn, awsClientConfig); LOGGER.info("Using STSAssumeRoleSessionCredentialsProvider with assume role " + assumeRoleArn); } // if credentials are set explicitly make them available to the AWS SDK if (StringUtils.isNotBlank(account) && StringUtils.isNotBlank(secret)) { this.exportCredentials(account, secret); } createClient(); createCalendar(); createScheduler(); createRecorder(); } /** * Checks whether it is safe to log the property based on the given * property key. * @param propertyKey The key for the property, expected to be resolvable to a String * @return A boolean indicating whether it is safe to log the corresponding property */ protected boolean isSafeToLog(Object propertyKey) { String propertyKeyName = propertyKey.toString(); return !propertyKeyName.contains("secretKey") && !propertyKeyName.contains("vsphere.password"); } /** loads the given config on top of the config read by previous calls. */ protected void loadConfigurationFileIntoProperties(String propertyFileName) { String propFile = System.getProperty(propertyFileName, "/" + propertyFileName); try { LOGGER.info("loading properties file: " + propFile); InputStream is = BasicSimianArmyContext.class.getResourceAsStream(propFile); try { properties.load(is); } finally { is.close(); } } catch (Exception e) { String msg = "Unable to load properties file " + propFile + " set System property \"" + propertyFileName + "\" to valid file"; LOGGER.error(msg); throw new RuntimeException(msg, e); } } private void createScheduler() { int freq = (int) config.getNumOrElse("simianarmy.scheduler.frequency", 1); TimeUnit freqUnit = TimeUnit.valueOf(config.getStrOrElse("simianarmy.scheduler.frequencyUnit", "HOURS")); int threads = (int) config.getNumOrElse("simianarmy.scheduler.threads", MONKEY_THREADS); setScheduler(new BasicScheduler(freq, freqUnit, threads)); } @SuppressWarnings("unchecked") private void createRecorder() { @SuppressWarnings("rawtypes") Class recorderClass = loadClientClass("simianarmy.client.recorder.class"); if (recorderClass != null && recorderClass.equals(RDSRecorder.class)) { String dbDriver = configuration().getStr("simianarmy.recorder.db.driver"); String dbUser = configuration().getStr("simianarmy.recorder.db.user"); String dbPass = configuration().getStr("simianarmy.recorder.db.pass"); String dbUrl = configuration().getStr("simianarmy.recorder.db.url"); String dbTable = configuration().getStr("simianarmy.recorder.db.table"); RDSRecorder rdsRecorder = new RDSRecorder(dbDriver, dbUser, dbPass, dbUrl, dbTable, client.region()); rdsRecorder.init(); setRecorder(rdsRecorder); } else if (recorderClass == null || recorderClass.equals(SimpleDBRecorder.class)) { String domain = config.getStrOrElse("simianarmy.recorder.sdb.domain", "SIMIAN_ARMY"); if (client != null) { SimpleDBRecorder simpleDbRecorder = new SimpleDBRecorder(client, domain); simpleDbRecorder.init(); setRecorder(simpleDbRecorder); } } else { setRecorder((MonkeyRecorder) factory(recorderClass)); } } @SuppressWarnings("unchecked") private void createCalendar() { @SuppressWarnings("rawtypes") Class calendarClass = loadClientClass("simianarmy.calendar.class"); if (calendarClass == null || calendarClass.equals(BasicCalendar.class)) { setCalendar(new BasicCalendar(config)); } else { setCalendar((MonkeyCalendar) factory(calendarClass)); } } /** * Create the specific client with region taken from properties. * Override to provide your own client. */ protected void createClient() { createClient(region); } /** * Create the specific client within passed region, using the appropriate AWS credentials provider * and client configuration. * @param clientRegion */ protected void createClient(String clientRegion) { this.client = new AWSClient(clientRegion, awsCredentialsProvider, awsClientConfig); setCloudClient(this.client); } /** * Gets the AWS client. * @return the AWS client */ public AWSClient awsClient() { return client; } /** * Gets the region. * @return the region */ public String region() { return region; } /** * Gets the accountName * @return the accountName */ public String accountName() { return accountName; } @Override public void reportEvent(Event evt) { this.eventReport.add(evt); } @Override public void resetEventReport() { eventReport.clear(); } @Override public String getEventReport() { StringBuilder report = new StringBuilder(); for (Event event : this.eventReport) { report.append(String.format("%s %s (", event.eventType(), event.id())); boolean isFirst = true; for (Entry field : event.fields().entrySet()) { if (!isFirst) { report.append(", "); } else { isFirst = false; } report.append(String.format("%s:%s", field.getKey(), field.getValue())); } report.append(")\n"); } return report.toString(); } /** * Exports credentials as Java system properties * to be picked up by AWS SDK clients. * @param accountKey * @param secretKey */ public void exportCredentials(String accountKey, String secretKey) { System.setProperty("aws.accessKeyId", accountKey); System.setProperty("aws.secretKey", secretKey); } /** {@inheritDoc} */ @Override public MonkeyScheduler scheduler() { return scheduler; } /** * Sets the scheduler. * * @param scheduler * the new scheduler */ protected void setScheduler(MonkeyScheduler scheduler) { this.scheduler = scheduler; } /** {@inheritDoc} */ @Override public MonkeyCalendar calendar() { return calendar; } /** * Sets the calendar. * * @param calendar * the new calendar */ protected void setCalendar(MonkeyCalendar calendar) { this.calendar = calendar; } /** {@inheritDoc} */ @Override public MonkeyConfiguration configuration() { return config; } /** * Sets the configuration. * * @param configuration * the new configuration */ protected void setConfiguration(MonkeyConfiguration configuration) { this.config = (BasicConfiguration) configuration; } /** {@inheritDoc} */ @Override public CloudClient cloudClient() { return client; } /** * Sets the cloud client. * * @param cloudClient * the new cloud client */ protected void setCloudClient(CloudClient cloudClient) { this.client = (AWSClient) cloudClient; } /** {@inheritDoc} */ @Override public MonkeyRecorder recorder() { return recorder; } /** * Sets the recorder. * * @param recorder * the new recorder */ protected void setRecorder(MonkeyRecorder recorder) { this.recorder = recorder; } /** * Gets the configuration properties. * @return the configuration properties */ protected Properties getProperties() { return this.properties; } /** * Gets the AWS credentials provider. * @return the AWS credentials provider */ public AWSCredentialsProvider getAwsCredentialsProvider() { return awsCredentialsProvider; } /** * Gets the AWS client configuration. * @return the AWS client configuration */ public ClientConfiguration getAwsClientConfig() { return awsClientConfig; } /** * Load a class specified by the config; for drop-in replacements. * (Duplicates a method in MonkeyServer; refactor to util?). * * @param key * @return the loaded class or null if the class is not found */ @SuppressWarnings("rawtypes") private Class loadClientClass(String key) { ClassLoader classLoader = getClass().getClassLoader(); try { String clientClassName = config.getStrOrElse(key, null); if (clientClassName == null || clientClassName.isEmpty()) { LOGGER.info("using standard class for " + key); return null; } Class newClass = classLoader.loadClass(clientClassName); LOGGER.info("using " + key + " loaded " + newClass.getCanonicalName()); return newClass; } catch (ClassNotFoundException e) { throw new RuntimeException("Could not load " + key, e); } } /** * Generic factory to create monkey collateral types. * * @param * the generic type to create * @param implClass * the actual concrete type to instantiate. * @return an object of the requested type */ private T factory(Class implClass) { try { // then find corresponding ctor for (Constructor ctor : implClass.getDeclaredConstructors()) { Class[] paramTypes = ctor.getParameterTypes(); if (paramTypes.length != 1) { continue; } if (paramTypes[0].getName().endsWith("Configuration")) { @SuppressWarnings("unchecked") T impl = (T) ctor.newInstance(config); return impl; } } // Last ditch; try no-arg. return implClass.newInstance(); } catch (Exception e) { LOGGER.error("context config error, cannot make an instance of " + implClass.getName(), e); } return null; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/LocalDbRecorder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentNavigableMap; import org.mapdb.Atomic; import org.mapdb.DB; import org.mapdb.DBMaker; import org.mapdb.Fun; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.MonkeyRecorder; import com.netflix.simianarmy.MonkeyType; import com.netflix.simianarmy.chaos.ChaosMonkey; /** * Replacement for SimpleDB on non-AWS: use an embedded db. * * @author jgardner * */ public class LocalDbRecorder implements MonkeyRecorder { private static DB db = null; private static Atomic.Long nextId = null; private static ConcurrentNavigableMap, Event> eventMap = null; // Upper bound, so we don't fill the disk with monkey events private static final double MAX_EVENTS = 10000; private double maxEvents = MAX_EVENTS; private String dbFilename = "simianarmy_events"; private String dbpassword = null; /** Constructor. * */ public LocalDbRecorder(MonkeyConfiguration configuration) { if (configuration != null) { dbFilename = configuration.getStrOrElse("simianarmy.recorder.localdb.file", null); maxEvents = configuration.getNumOrElse("simianarmy.recorder.localdb.max_events", MAX_EVENTS); dbpassword = configuration.getStrOrElse("simianarmy.recorder.localdb.password", null); } } private synchronized void init() { if (nextId != null) { return; } File dbFile = null; dbFile = (dbFilename == null) ? tempDbFile() : new File(dbFilename); if (dbpassword != null) { db = DBMaker.newFileDB(dbFile) .closeOnJvmShutdown() .encryptionEnable(dbpassword) .make(); } else { db = DBMaker.newFileDB(dbFile) .closeOnJvmShutdown() .make(); } eventMap = db.getTreeMap("eventMap"); nextId = db.createAtomicLong("next", 1); } private static File tempDbFile() { try { final File tmpFile = File.createTempFile("mapdb", "db"); tmpFile.deleteOnExit(); return tmpFile; } catch (IOException e) { throw new RuntimeException("Temporary DB file could not be created", e); } } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder#newEvent(MonkeyType, EventType, String, String) */ @Override public Event newEvent(MonkeyType monkeyType, EventType eventType, String region, String id) { init(); return new MapDbRecorderEvent(monkeyType, eventType, region, id); } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder#recordEvent(com.netflix.simianarmy.MonkeyRecorder.Event) */ @Override public void recordEvent(Event evt) { init(); Fun.Tuple2 id = Fun.t2(evt.eventTime().getTime(), nextId.incrementAndGet()); if (eventMap.size() + 1 > maxEvents) { eventMap.remove(eventMap.firstKey()); } eventMap.put(id, evt); db.commit(); } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder#findEvents(java.util.Map, java.util.Date) */ @Override public List findEvents(Map query, Date after) { init(); List foundEvents = new ArrayList(); for (Event evt : eventMap.tailMap(toKey(after)).values()) { boolean matched = true; for (Map.Entry pair : query.entrySet()) { if (pair.getKey().equals("id") && !evt.id().equals(pair.getValue())) { matched = false; } if (pair.getKey().equals("monkeyType") && !evt.monkeyType().toString().equals(pair.getValue())) { matched = false; } if (pair.getKey().equals("eventType") && !evt.eventType().toString().equals(pair.getValue())) { matched = false; } } if (matched) { foundEvents.add(evt); } } return foundEvents; } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder#findEvents(MonkeyType, Map, Date) */ @Override public List findEvents(MonkeyType monkeyType, Map query, Date after) { Map copy = new LinkedHashMap(query); copy.put("monkeyType", monkeyType.name()); return findEvents(copy, after); } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder#findEvents(MonkeyType, EventType, Map, Date) */ @Override public List findEvents(MonkeyType monkeyType, EventType eventType, Map query, Date after) { Map copy = new LinkedHashMap(query); copy.put("monkeyType", monkeyType.name()); copy.put("eventType", eventType.name()); return findEvents(copy, after); } private Fun.Tuple2 toKey(Date date) { return Fun.t2(date.getTime(), 0L); } /** Loggable event for LocalDbRecorder. * */ public static class MapDbRecorderEvent implements MonkeyRecorder.Event, Serializable { /** The monkey type. */ private MonkeyType monkeyType; /** The event type. */ private EventType eventType; /** The event id. */ private String id; /** The event region. */ private String region; /** The fields. */ private Map fields = new HashMap(); /** The event time. */ private Date date; private static final long serialVersionUID = 1L; /** Constructor. * @param monkeyType * @param eventType * @param region * @param id */ public MapDbRecorderEvent(MonkeyType monkeyType, EventType eventType, String region, String id) { this.monkeyType = monkeyType; this.eventType = eventType; this.id = id; this.region = region; this.date = new Date(); } /** Constructor. * @param monkeyType * @param eventType * @param region * @param id * @param time */ public MapDbRecorderEvent(MonkeyType monkeyType, EventType eventType, String region, String id, long time) { this.monkeyType = monkeyType; this.eventType = eventType; this.id = id; this.region = region; this.date = new Date(time); } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder.Event#id() */ @Override public String id() { return id; } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder.Event#eventTime() */ @Override public Date eventTime() { return new Date(date.getTime()); } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder.Event#monkeyType() */ @Override public MonkeyType monkeyType() { return monkeyType; } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder.Event#eventType() */ @Override public EventType eventType() { return eventType; } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder.Event#region() */ @Override public String region() { return region; } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder.Event#fields() */ @Override public Map fields() { return Collections.unmodifiableMap(fields); } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder.Event#field(java.lang.String) */ @Override public String field(String name) { return fields.get(name); } /* (non-Javadoc) * @see com.netflix.simianarmy.MonkeyRecorder.Event#addField(java.lang.String, java.lang.String) */ @Override public Event addField(String name, String value) { fields.put(name, value); return this; } } /** Appears to be used for testing, if so should be moved to a unit test. (2/16/2014, mgeis) * @param args */ public static void main(String[] args) { LocalDbRecorder r = new LocalDbRecorder(null); r.init(); List events2 = r.findEvents(new HashMap(), new Date(0)); for (Event event : events2) { System.out.println("Got:" + event + ": " + event.eventTime().getTime()); } for (int i = 0; i < 10; i++) { Event event = r.newEvent(ChaosMonkey.Type.CHAOS, ChaosMonkey.EventTypes.CHAOS_TERMINATION, "1", "1"); r.recordEvent(event); System.out.println("Added:" + event + ": " + event.eventTime().getTime()); } List events = r.findEvents(new HashMap(), new Date(0)); for (Event event : events) { System.out.println("Got:" + event + ": " + event.eventTime().getTime()); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/calendars/BavarianCalendar.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.calendars; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.basic.BasicCalendar; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; // CHECKSTYLE IGNORE MagicNumberCheck /** * The Class BavarianCalendar. */ public class BavarianCalendar extends BasicCalendar { /** * Instantiates a new basic calendar. * * @param cfg the monkey configuration */ public BavarianCalendar(MonkeyConfiguration cfg) { super(cfg); } /** {@inheritDoc} */ @Override protected void loadHolidays(int year) { holidays.clear(); // these aren't all strictly holidays, but days when engineers will likely // not be in the office to respond to rampaging monkeys // first of all, we need easter sunday doy, // because ome other holidays calculated from it int easter = westernEasterDayOfYear(year); // new year holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.JANUARY, 1))); // epiphanie holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.JANUARY, 6))); // good friday, always friday, don't need to check if it's bridge day holidays.add(easter - 2); // easter monday, always monday, don't need to check if it's bridge day holidays.add(easter + 1); // labor day holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.MAY, 1))); // ascension day holidays.addAll(getHolidayWithBridgeDays(year, easter + 39)); // whit monday, always monday, don't need to check if it's bridge day holidays.add(easter + 50); // corpus christi holidays.add(westernEasterDayOfYear(year) + 60); // assumption day holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.AUGUST, 15))); // german unity day holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.OCTOBER, 3))); // all saints holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.NOVEMBER, 1))); // monkey goes on christmas vacations between christmas and new year! holidays.addAll(getHolidayWithBridgeDays(year, dayOfYear(year, Calendar.DECEMBER, 24))); holidays.add(dayOfYear(year, Calendar.DECEMBER, 25)); holidays.add(dayOfYear(year, Calendar.DECEMBER, 26)); holidays.add(dayOfYear(year, Calendar.DECEMBER, 27)); holidays.add(dayOfYear(year, Calendar.DECEMBER, 28)); holidays.add(dayOfYear(year, Calendar.DECEMBER, 29)); holidays.add(dayOfYear(year, Calendar.DECEMBER, 30)); holidays.add(dayOfYear(year, Calendar.DECEMBER, 31)); // mark the holiday set with the year, so on Jan 1 it will automatically // recalculate the holidays for next year holidays.add(year); } /** * Returns collection of holidays, including Monday or Friday * if given holiday is Thuesday or Thursday. * * The behaviour to take Monday as day off if official holiday is Thuesday * and to take Friday as day off if official holiday is Thursday * is specific to [at least] Germany. * We call it, literally, "bridge day". * * @param dayOfYear holiday day of year */ private Collection getHolidayWithBridgeDays(int year, int dayOfYear) { Calendar holiday = now(); holiday.set(Calendar.YEAR, year); holiday.set(Calendar.DAY_OF_YEAR, dayOfYear); int dow = holiday.get(Calendar.DAY_OF_WEEK); int mon = holiday.get(Calendar.MONTH); int dom = holiday.get(Calendar.DAY_OF_MONTH); // We don't want to include Monday if Thuesday is January 1. if (dow == Calendar.TUESDAY && dayOfYear != 1) return Arrays.asList(dayOfYear, dayOfYear - 1); // We don't want to include Friday if Thursday is December 31. if (dow == Calendar.THURSDAY && (mon != Calendar.DECEMBER || dom != 31)) return Arrays.asList(dayOfYear, dayOfYear + 1); return Arrays.asList(dayOfYear); } /** * Western easter sunday in year. * * @param year * the year * @return the day of the year of western easter sunday */ protected int westernEasterDayOfYear(int year) { int a = year % 19, b = year / 100, c = year % 100, d = b / 4, e = b % 4, g = (8 * b + 13) / 25, h = (19 * a + b - d - g + 15) % 30, j = c / 4, k = c % 4, m = (a + 11 * h) / 319, r = (2 * e + 2 * j - k - h + m + 32) % 7; int oneBasedMonth = (h - m + r + 90) / 25; int dayOfYear = (h - m + r + oneBasedMonth + 19) % 32; return dayOfYear(year, oneBasedMonth - 1, dayOfYear); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/chaos/BasicChaosEmailNotifier.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.chaos; import java.util.Arrays; import java.util.List; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import com.netflix.simianarmy.chaos.ChaosEmailNotifier; import com.netflix.simianarmy.chaos.ChaosType; /** The basic implementation of the email notifier for Chaos monkey. * */ public class BasicChaosEmailNotifier extends ChaosEmailNotifier { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicChaosEmailNotifier.class); private final MonkeyConfiguration cfg; private final String defaultEmail; private final List ccAddresses; /** Constructor. * * @param cfg the monkey configuration * @param sesClient the Amazon SES client * @param defaultEmail the default email address to notify when the group does not have a * owner email specified * @param ccAddresses the cc email addresses for notifications */ public BasicChaosEmailNotifier(MonkeyConfiguration cfg, AmazonSimpleEmailServiceClient sesClient, String defaultEmail, String... ccAddresses) { super(sesClient); this.cfg = cfg; this.defaultEmail = defaultEmail; this.ccAddresses = Arrays.asList(ccAddresses); } /** * Sends an email notification for a termination of instance to a global * email address. * @param group the instance group * @param instanceId the instance id * @param chaosType the chosen chaos strategy */ @Override public void sendTerminationGlobalNotification(InstanceGroup group, String instanceId, ChaosType chaosType) { String to = cfg.getStr("simianarmy.chaos.notification.global.receiverEmail"); if (StringUtils.isBlank(to)) { LOGGER.warn("Global email address was not set, but global email notification was enabled!"); return; } LOGGER.info("sending termination notification to global email address {}", to); buildAndSendEmail(to, group, instanceId, chaosType); } /** * Sends an email notification for a termination of instance to the group * owner's email address. * @param group the instance group * @param instanceId the instance id * @param chaosType the chosen chaos strategy */ @Override public void sendTerminationNotification(InstanceGroup group, String instanceId, ChaosType chaosType) { String to = getOwnerEmail(group); LOGGER.info("sending termination notification to group owner email address {}", to); buildAndSendEmail(to, group, instanceId, chaosType); } /** * Gets the owner's email for a instance group. * @param group the instance group * @return the owner email of the instance group */ protected String getOwnerEmail(InstanceGroup group) { String prop = String.format("simianarmy.chaos.%s.%s.ownerEmail", group.type(), group.name()); String ownerEmail = cfg.getStr(prop); if (ownerEmail == null) { LOGGER.info(String.format("Property %s is not set, use the default email address %s as" + " the owner email of group %s of type %s", prop, defaultEmail, group.name(), group.type())); return defaultEmail; } else { return ownerEmail; } } /** * Builds the body and subject for the email, sends the email. * @param group * the instance group * @param instanceId * the instance id * @param to * the email address to be sent to * @param chaosType the chosen chaos strategy */ public void buildAndSendEmail(String to, InstanceGroup group, String instanceId, ChaosType chaosType) { String body = buildEmailBody(group, instanceId, chaosType); String subject; boolean emailSubjectIsBody = cfg.getBoolOrElse( "simianarmy.chaos.notification.subject.isBody", false); if (emailSubjectIsBody) { subject = body; } else { subject = buildEmailSubject(to); } sendEmail(to, subject, body); } @Override public String buildEmailSubject(String to) { String emailSubjectPrefix = cfg.getStrOrElse("simianarmy.chaos.notification.subject.prefix", ""); String emailSubjectSuffix = cfg.getStrOrElse("simianarmy.chaos.notification.subject.suffix", ""); return String.format("%sChaos Monkey Termination Notification for %s%s", emailSubjectPrefix, to, emailSubjectSuffix); } /** * Builds the body for the email. * @param group * the instance group * @param instanceId * the instance id * @param chaosType the chosen chaos strategy * @return the created string */ public String buildEmailBody(InstanceGroup group, String instanceId, ChaosType chaosType) { String emailBodyPrefix = cfg.getStrOrElse("simianarmy.chaos.notification.body.prefix", ""); String emailBodySuffix = cfg.getStrOrElse("simianarmy.chaos.notification.body.suffix", ""); String body = emailBodyPrefix; body += String.format("Instance %s of %s %s is being terminated by Chaos monkey.", instanceId, group.type(), group.name()); if (chaosType != null) { body += "\n"; body += String.format("Chaos type: %s.", chaosType.getKey()); } body += emailBodySuffix; return body; } @Override public String[] getCcAddresses(String to) { return ccAddresses.toArray(new String[ccAddresses.size()]); } @Override public String getSourceAddress(String to) { String prop = "simianarmy.chaos.notification.sourceEmail"; String sourceEmail = cfg.getStr(prop); if (sourceEmail == null || !isValidEmail(sourceEmail)) { String msg = String.format("Property %s is not set or its value is not a valid email.", prop); LOGGER.error(msg); throw new RuntimeException(msg); } return sourceEmail; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/chaos/BasicChaosInstanceSelector.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.chaos; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Random; import com.google.common.collect.Lists; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import com.netflix.simianarmy.chaos.ChaosInstanceSelector; /** * The Class BasicChaosInstanceSelector. */ public class BasicChaosInstanceSelector implements ChaosInstanceSelector { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicChaosInstanceSelector.class); /** The Constant RANDOM. */ private static final Random RANDOM = new Random(); /** * Logger, this is abstracted so subclasses (for testing) can reset logger to make it less verbose. * @return the logger */ protected Logger logger() { return LOGGER; } /** {@inheritDoc} */ @Override public Collection select(InstanceGroup group, double probability) { int n = ((int) probability); String selected = selectOneInstance(group, probability - n); Collection result = selectNInstances(group.instances(), n, selected); if (selected != null) { result.add(selected); } return result; } private Collection selectNInstances(Collection instances, int n, String selected) { logger().info("Randomly selecting {} from {} instances, excluding {}", new Object[] {n, instances.size(), selected}); List copy = Lists.newArrayList(); for (String instance : instances) { if (!instance.equals(selected)) { copy.add(instance); } } if (n >= copy.size()) { return copy; } Collections.shuffle(copy); return copy.subList(0, n); } private String selectOneInstance(InstanceGroup group, double probability) { Validate.isTrue(probability < 1); if (probability <= 0) { logger().info("Group {} [type {}] has disabled probability: {}", new Object[] {group.name(), group.type(), probability}); return null; } double rand = Math.random(); if (rand > probability || group.instances().isEmpty()) { logger().info("Group {} [type {}] got lucky: {} > {}", new Object[] {group.name(), group.type(), rand, probability}); return null; } return group.instances().get(RANDOM.nextInt(group.instances().size())); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/chaos/BasicChaosMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.chaos; import com.google.common.collect.Lists; import com.netflix.simianarmy.*; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.chaos.*; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.TimeUnit; /** * The Class BasicChaosMonkey. */ public class BasicChaosMonkey extends ChaosMonkey { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicChaosMonkey.class); /** The Constant NS. */ private static final String NS = "simianarmy.chaos."; /** The cfg. */ private final MonkeyConfiguration cfg; /** The runs per day. */ private final long runsPerDay; /** The minimum value of the maxTerminationCountPerday property to be considered non-zero. **/ private static final double MIN_MAX_TERMINATION_COUNT_PER_DAY = 0.001; private final MonkeyCalendar monkeyCalendar; // When a mandatory termination is triggered due to the minimum termination limit is breached, // the value below is used as the termination probability. private static final double DEFAULT_MANDATORY_TERMINATION_PROBABILITY = 0.5; private final List allChaosTypes; /** * Instantiates a new basic chaos monkey. * @param ctx * the ctx */ public BasicChaosMonkey(ChaosMonkey.Context ctx) { super(ctx); this.cfg = ctx.configuration(); this.monkeyCalendar = ctx.calendar(); Calendar open = monkeyCalendar.now(); Calendar close = monkeyCalendar.now(); open.set(Calendar.HOUR, monkeyCalendar.openHour()); close.set(Calendar.HOUR, monkeyCalendar.closeHour()); allChaosTypes = Lists.newArrayList(); allChaosTypes.add(new ShutdownInstanceChaosType(cfg)); allChaosTypes.add(new BlockAllNetworkTrafficChaosType(cfg)); allChaosTypes.add(new DetachVolumesChaosType(cfg)); allChaosTypes.add(new BurnCpuChaosType(cfg)); allChaosTypes.add(new BurnIoChaosType(cfg)); allChaosTypes.add(new KillProcessesChaosType(cfg)); allChaosTypes.add(new NullRouteChaosType(cfg)); allChaosTypes.add(new FailEc2ChaosType(cfg)); allChaosTypes.add(new FailDnsChaosType(cfg)); allChaosTypes.add(new FailDynamoDbChaosType(cfg)); allChaosTypes.add(new FailS3ChaosType(cfg)); allChaosTypes.add(new FillDiskChaosType(cfg)); allChaosTypes.add(new NetworkCorruptionChaosType(cfg)); allChaosTypes.add(new NetworkLatencyChaosType(cfg)); allChaosTypes.add(new NetworkLossChaosType(cfg)); TimeUnit freqUnit = ctx.scheduler().frequencyUnit(); if (TimeUnit.DAYS == freqUnit) { runsPerDay = ctx.scheduler().frequency(); } else { long units = freqUnit.convert(close.getTimeInMillis() - open.getTimeInMillis(), TimeUnit.MILLISECONDS); runsPerDay = units / ctx.scheduler().frequency(); } } /** {@inheritDoc} */ @Override public void doMonkeyBusiness() { context().resetEventReport(); cfg.reload(); if (!isChaosMonkeyEnabled()) { return; } for (InstanceGroup group : context().chaosCrawler().groups()) { if (isGroupEnabled(group)) { if (isMaxTerminationCountExceeded(group)) { continue; } double prob = getEffectiveProbability(group); Collection instances = context().chaosInstanceSelector().select(group, prob / runsPerDay); for (String inst : instances) { if (isMaxTerminationCountExceeded(group)) { break; } ChaosType chaosType = pickChaosType(context().cloudClient(), inst); if (chaosType == null) { // This is surprising ... normally we can always just terminate it LOGGER.warn("No chaos type was applicable to the instance: {}", inst); continue; } terminateInstance(group, inst, chaosType); } } } } private ChaosType pickChaosType(CloudClient cloudClient, String instanceId) { Random random = new Random(); SshConfig sshConfig = new SshConfig(cfg); ChaosInstance instance = new ChaosInstance(cloudClient, instanceId, sshConfig); List applicable = Lists.newArrayList(); for (ChaosType chaosType : allChaosTypes) { if (chaosType.isEnabled() && chaosType.canApply(instance)) { applicable.add(chaosType); } } if (applicable.isEmpty()) { return null; } int index = random.nextInt(applicable.size()); return applicable.get(index); } @Override public Event terminateNow(String type, String name, ChaosType chaosType) throws FeatureNotEnabledException, InstanceGroupNotFoundException { Validate.notNull(type); Validate.notNull(name); cfg.reload(name); if (!isChaosMonkeyEnabled()) { String msg = String.format("Chaos monkey is not enabled for group %s [type %s]", name, type); LOGGER.info(msg); throw new FeatureNotEnabledException(msg); } String prop = NS + "terminateOndemand.enabled"; if (cfg.getBool(prop)) { InstanceGroup group = findInstanceGroup(type, name); if (group == null) { throw new InstanceGroupNotFoundException(type, name); } Collection instances = context().chaosInstanceSelector().select(group, 1.0); Validate.isTrue(instances.size() <= 1); if (instances.size() == 1) { return terminateInstance(group, instances.iterator().next(), chaosType); } else { throw new NotFoundException(String.format("No instance is found in group %s [type %s]", name, type)); } } else { String msg = String.format("Group %s [type %s] does not allow on-demand termination, set %s=true", name, type, prop); LOGGER.info(msg); throw new FeatureNotEnabledException(msg); } } private void reportEventForSummary(EventTypes eventType, InstanceGroup group, String instanceId) { context().reportEvent(createEvent(eventType, group, instanceId)); } /** * Handle termination error. This has been abstracted so subclasses can decide to continue causing chaos if desired. * * @param instance * the instance * @param e * the exception */ protected void handleTerminationError(String instance, Throwable e) { LOGGER.error("failed to terminate instance " + instance, e); throw new RuntimeException("failed to terminate instance " + instance, e); } /** {@inheritDoc} */ @Override public Event recordTermination(InstanceGroup group, String instance, ChaosType chaosType) { Event evt = context().recorder().newEvent(Type.CHAOS, EventTypes.CHAOS_TERMINATION, group.region(), instance); evt.addField("groupType", group.type().name()); evt.addField("groupName", group.name()); evt.addField("chaosType", chaosType.getKey()); context().recorder().recordEvent(evt); return evt; } /** {@inheritDoc} */ @Override public int getPreviousTerminationCount(InstanceGroup group, Date after) { Map query = new HashMap(); query.put("groupType", group.type().name()); query.put("groupName", group.name()); List evts = context().recorder().findEvents(Type.CHAOS, EventTypes.CHAOS_TERMINATION, query, after); return evts.size(); } private Event createEvent(EventTypes chaosTermination, InstanceGroup group, String instance) { Event evt = context().recorder().newEvent(Type.CHAOS, chaosTermination, group.region(), instance); evt.addField("groupType", group.type().name()); evt.addField("groupName", group.name()); return evt; } /** * Gets the effective probability value, returns 0 if the group is not enabled. Otherwise calls * getEffectiveProbability. * @param group * @return the effective probability value for the instance group */ protected double getEffectiveProbability(InstanceGroup group) { if (!isGroupEnabled(group)) { return 0; } return getEffectiveProbabilityFromCfg(group); } /** * Gets the effective probability value when the monkey processes an instance group, it uses the following * logic in the order as listed below. * * 1) When minimum mandatory termination is enabled, a default non-zero probability is used for opted-in * groups, if a) the application has been opted in for the last mandatory termination window * and b) there was no terminations in the last mandatory termination window * 2) Use the probability configured for the group type and name * 3) Use the probability configured for the group * 4) Use 1.0 * @param group * @return double */ protected double getEffectiveProbabilityFromCfg(InstanceGroup group) { String propName; if (cfg.getBool(NS + "mandatoryTermination.enabled")) { String mtwProp = NS + "mandatoryTermination.windowInDays"; int mandatoryTerminationWindowInDays = (int) cfg.getNumOrElse(mtwProp, 0); if (mandatoryTerminationWindowInDays > 0 && noTerminationInLastWindow(group, mandatoryTerminationWindowInDays)) { double mandatoryProb = cfg.getNumOrElse(NS + "mandatoryTermination.defaultProbability", DEFAULT_MANDATORY_TERMINATION_PROBABILITY); LOGGER.info("There has been no terminations for group {} [type {}] in the last {} days," + "setting the probability to {} for mandatory termination.", new Object[]{group.name(), group.type(), mandatoryTerminationWindowInDays, mandatoryProb}); return mandatoryProb; } } propName = "probability"; double prob = getNumFromCfgOrDefault(group, propName, 1.0); LOGGER.info("Group {} [type {}] enabled [prob {}]", new Object[]{group.name(), group.type(), prob}); return prob; } protected double getNumFromCfgOrDefault(InstanceGroup group, String propName, double defaultValue) { String defaultProp = String.format("%s%s.%s", NS, group.type(), propName); String prop = String.format("%s%s.%s.%s", NS, group.type(), group.name(), propName); return cfg.getNumOrElse(prop, cfg.getNumOrElse(defaultProp, defaultValue)); } protected boolean getBoolFromCfgOrDefault(InstanceGroup group, String propName, boolean defaultValue) { String defaultProp = String.format("%s%s.%s", NS, group.type(), propName); String prop = String.format("%s%s.%s.%s", NS, group.type(), group.name(), propName); return cfg.getBoolOrElse(prop, cfg.getBoolOrElse(defaultProp, defaultValue)); } /** * Returns lastOptInTimeInMilliseconds from the .properties file. * * @param group * @return long */ protected long getLastOptInMilliseconds(InstanceGroup group) { String prop = NS + group.type() + "." + group.name() + ".lastOptInTimeInMilliseconds"; long lastOptInTimeInMilliseconds = (long) cfg.getNumOrElse(prop, -1); return lastOptInTimeInMilliseconds; } private boolean noTerminationInLastWindow(InstanceGroup group, int mandatoryTerminationWindowInDays) { long lastOptInTimeInMilliseconds = getLastOptInMilliseconds(group); if (lastOptInTimeInMilliseconds < 0) { return false; } Calendar windowStart = monkeyCalendar.now(); windowStart.add(Calendar.DATE, -1 * mandatoryTerminationWindowInDays); // return true if the window start is after the last opt-in time and // there has been no termination since the window start if (windowStart.getTimeInMillis() > lastOptInTimeInMilliseconds && getPreviousTerminationCount(group, windowStart.getTime()) <= 0) { return true; } return false; } /** * Checks to see if the given instance group is enabled. * @param group * @return boolean */ protected boolean isGroupEnabled(InstanceGroup group) { boolean enabled = getBoolFromCfgOrDefault(group, "enabled", false); if (enabled) { return true; } else { String prop = NS + group.type() + "." + group.name() + ".enabled"; String defaultProp = NS + group.type() + ".enabled"; LOGGER.info("Group {} [type {}] disabled, set {}=true or {}=true", new Object[]{group.name(), group.type(), prop, defaultProp}); return false; } } private boolean isChaosMonkeyEnabled() { String prop = NS + "enabled"; if (cfg.getBoolOrElse(prop, true)) { return true; } LOGGER.info("ChaosMonkey disabled, set {}=true", prop); return false; } private InstanceGroup findInstanceGroup(String type, String name) { // Calling context().chaosCrawler().groups(name) causes a new crawl to get // the up to date information for the group name. for (InstanceGroup group : context().chaosCrawler().groups(name)) { if (group.type().toString().equals(type) && group.name().equals(name)) { return group; } } LOGGER.warn("Failed to find instance group for type {} and name {}", type, name); return null; } protected Event terminateInstance(InstanceGroup group, String inst, ChaosType chaosType) { Validate.notNull(group); Validate.notEmpty(inst); String prop = NS + "leashed"; if (cfg.getBoolOrElse(prop, true)) { LOGGER.info("leashed ChaosMonkey prevented from killing {} from group {} [{}], set {}=false", new Object[]{inst, group.name(), group.type(), prop}); reportEventForSummary(EventTypes.CHAOS_TERMINATION_SKIPPED, group, inst); return null; } else { try { Event evt = recordTermination(group, inst, chaosType); sendTerminationNotification(group, inst, chaosType); SshConfig sshConfig = new SshConfig(cfg); ChaosInstance chaosInstance = new ChaosInstance(context().cloudClient(), inst, sshConfig); chaosType.apply(chaosInstance); LOGGER.info("Terminated {} from group {} [{}] with {}", new Object[]{inst, group.name(), group.type(), chaosType.getKey() }); reportEventForSummary(EventTypes.CHAOS_TERMINATION, group, inst); return evt; } catch (NotFoundException e) { LOGGER.warn("Failed to terminate " + inst + ", it does not exist. Perhaps it was already terminated"); reportEventForSummary(EventTypes.CHAOS_TERMINATION_SKIPPED, group, inst); return null; } catch (Exception e) { handleTerminationError(inst, e); reportEventForSummary(EventTypes.CHAOS_TERMINATION_SKIPPED, group, inst); return null; } } } /** * Checks to see if the maximum termination window has been exceeded. * * @param group * @return boolean */ protected boolean isMaxTerminationCountExceeded(InstanceGroup group) { Validate.notNull(group); String propName = "maxTerminationsPerDay"; double maxTerminationsPerDay = getNumFromCfgOrDefault(group, propName, 1.0); if (maxTerminationsPerDay <= MIN_MAX_TERMINATION_COUNT_PER_DAY) { String prop = String.format("%s%s.%s.%s", NS, group.type(), group.name(), propName); LOGGER.info("ChaosMonkey is configured to not allow any killing from group {} [{}] " + "with max daily count set as {}", new Object[]{group.name(), group.type(), prop}); return true; } else { int daysBack = 1; int maxCount = (int) maxTerminationsPerDay; if (maxTerminationsPerDay < 1.0) { daysBack = (int) Math.ceil(1 / maxTerminationsPerDay); maxCount = 1; } Calendar after = monkeyCalendar.now(); after.add(Calendar.DATE, -1 * daysBack); // Check if the group has exceeded the maximum terminations for the last period int terminationCount = getPreviousTerminationCount(group, after.getTime()); if (terminationCount >= maxCount) { LOGGER.info("The count of terminations for group {} [{}] in the last {} days is {}," + " equal or greater than the max count threshold {}", new Object[]{group.name(), group.type(), daysBack, terminationCount, maxCount}); return true; } } return false; } @Override public void sendTerminationNotification(InstanceGroup group, String instance, ChaosType chaosType) { String propEmailGlobalEnabled = "simianarmy.chaos.notification.global.enabled"; String propEmailGroupEnabled = String.format("%s%s.%s.notification.enabled", NS, group.type(), group.name()); ChaosEmailNotifier notifier = context().chaosEmailNotifier(); if (notifier == null) { String msg = "Chaos email notifier is not set."; LOGGER.error(msg); throw new RuntimeException(msg); } if (cfg.getBoolOrElse(propEmailGroupEnabled, false)) { notifier.sendTerminationNotification(group, instance, chaosType); } if (cfg.getBoolOrElse(propEmailGlobalEnabled, false)) { notifier.sendTerminationGlobalNotification(group, instance, chaosType); } } /** * {@inheritDoc} */ @Override public List getChaosTypes() { return Lists.newArrayList(allChaosTypes); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/chaos/BasicInstanceGroup.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.chaos; import java.util.Collections; import java.util.LinkedList; import java.util.List; import com.amazonaws.services.autoscaling.model.TagDescription; import com.netflix.simianarmy.GroupType; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; /** * The Class BasicInstanceGroup. */ public class BasicInstanceGroup implements InstanceGroup { /** The name. */ private final String name; /** The type. */ private final GroupType type; /** The region. */ private final String region; /** list of the tags of the ASG */ private final List tags; /** * Instantiates a new basic instance group. * * @param name * the name * @param type * the type * @param tags * the ASG tags */ public BasicInstanceGroup(String name, GroupType type, String region, List tags) { this.name = name; this.type = type; this.region = region; this.tags = tags; } /** {@inheritDoc} */ public GroupType type() { return type; } /** {@inheritDoc} */ public String name() { return name; } /** {@inheritDoc} */ public String region() { return region; } /** {@inheritDoc} */ public List tags() { return tags; } /** The list. */ private List list = new LinkedList(); /** {@inheritDoc} */ @Override public List instances() { return Collections.unmodifiableList(list); } /** {@inheritDoc} */ @Override public void addInstance(String instance) { list.add(instance); } /** {@inheritDoc} */ @Override public BasicInstanceGroup copyAs(String newName) { BasicInstanceGroup newGroup = new BasicInstanceGroup(newName, this.type(), this.region(), this.tags()); for (String instance: this.instances()) { newGroup.addInstance(instance); } return newGroup; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/chaos/CloudFormationChaosMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.chaos; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import com.netflix.simianarmy.chaos.ChaosType; /** * The Class CloudFormationChaosMonkey. Strips out the random string generated by the CloudFormation api in * the instance group name of the ASG we want to kill instances on */ public class CloudFormationChaosMonkey extends BasicChaosMonkey { /** * Instantiates a new cloud formation chaos monkey. * @param ctx * the ctx */ public CloudFormationChaosMonkey(Context ctx) { super(ctx); } /** * {@inheritDoc} */ @Override protected boolean isGroupEnabled(InstanceGroup group) { InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group); return super.isGroupEnabled(noSuffixGroup); } /** * {@inheritDoc} */ @Override protected Event terminateInstance(InstanceGroup group, String inst, ChaosType chaosType) { InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group); return super.terminateInstance(noSuffixGroup, inst, chaosType); } /** * {@inheritDoc} */ @Override protected boolean isMaxTerminationCountExceeded(InstanceGroup group) { InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group); return super.isMaxTerminationCountExceeded(noSuffixGroup); } /** * {@inheritDoc} */ @Override protected double getEffectiveProbability(InstanceGroup group) { InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group); if (!super.isGroupEnabled(noSuffixGroup)) { return 0; } return getEffectiveProbabilityFromCfg(noSuffixGroup); } /** * Returns the lastOptInTimeInMilliseconds parameter for a group omitting the * randomly generated suffix. */ @Override protected long getLastOptInMilliseconds(InstanceGroup group) { InstanceGroup noSuffixGroup = noSuffixInstanceGroup(group); return super.getLastOptInMilliseconds(noSuffixGroup); } /** * Return a copy of the instance group removing the randomly generated suffix from * its name. */ public InstanceGroup noSuffixInstanceGroup(InstanceGroup group) { String newName = group.name().replaceAll("(-)([^-]*$)", ""); InstanceGroup noSuffixGroup = group.copyAs(newName); return noSuffixGroup; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityEmailBuilder.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.conformity; import com.google.common.collect.Maps; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import com.netflix.simianarmy.conformity.ConformityEmailBuilder; import com.netflix.simianarmy.conformity.ConformityRule; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Map; /** The basic implementation of the email builder for Conformity monkey. */ public class BasicConformityEmailBuilder extends ConformityEmailBuilder { private static final String[] TABLE_COLUMNS = {"Cluster", "Region", "Rule", "Failed Components"}; private static final String AHREF_TEMPLATE = "%s"; private static final Logger LOGGER = LoggerFactory.getLogger(BasicConformityEmailBuilder.class); private Map> emailToClusters; private final Map idToRule = Maps.newHashMap(); @Override public void setEmailToClusters(Map> clustersByEmail, Collection rules) { Validate.notNull(clustersByEmail); Validate.notNull(rules); this.emailToClusters = clustersByEmail; idToRule.clear(); for (ConformityRule rule : rules) { idToRule.put(rule.getName(), rule); } } @Override protected String getHeader() { StringBuilder header = new StringBuilder(); header.append("

Conformity Report

"); header.append("The following is a list of failed conformity rules for your cluster(s).
"); return header.toString(); } @Override protected String getEntryTable(String emailAddress) { StringBuilder table = new StringBuilder(); table.append(getHtmlTableHeader(getTableColumns())); for (Cluster cluster : emailToClusters.get(emailAddress)) { for (Conformity conformity : cluster.getConformties()) { if (!conformity.getFailedComponents().isEmpty()) { table.append(getClusterRow(cluster, conformity)); } } } table.append("
"); return table.toString(); } @Override protected String getFooter() { return "
Conformity Monkey wiki: https://github.com/Netflix/SimianArmy/wiki
"; } /** * Gets the url to view the details of the cluster. * @param cluster the cluster * @return the url to view/edit the cluster. */ protected String getClusterUrl(Cluster cluster) { return null; } /** * Gets the string when displaying the cluster, e.g. the id. * @param cluster the cluster to display * @return the string to represent the cluster */ protected String getClusterDisplay(Cluster cluster) { return cluster.getName(); } /** Gets the table columns for the table in the email. * * @return the array of column names */ protected String[] getTableColumns() { return TABLE_COLUMNS; } /** * Gets the row for a cluster and a failed conformity check in the table in the email body. * @param cluster the cluster to display * @param conformity the failed conformity check * @return the table row in the email body */ protected String getClusterRow(Cluster cluster, Conformity conformity) { StringBuilder message = new StringBuilder(); message.append(""); String clusterUrl = getClusterUrl(cluster); if (!StringUtils.isEmpty(clusterUrl)) { message.append(getHtmlCell(String.format(AHREF_TEMPLATE, clusterUrl, getClusterDisplay(cluster)))); } else { message.append(getHtmlCell(getClusterDisplay(cluster))); } message.append(getHtmlCell(cluster.getRegion())); ConformityRule rule = idToRule.get(conformity.getRuleId()); String ruleDesc; if (rule == null) { LOGGER.warn(String.format("Not found rule with name %s", conformity.getRuleId())); ruleDesc = conformity.getRuleId(); } else { ruleDesc = rule.getNonconformingReason(); } message.append(getHtmlCell(ruleDesc)); message.append(getHtmlCell(StringUtils.join(conformity.getFailedComponents(), ","))); message.append(""); return message.toString(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkey.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.conformity; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.ClusterCrawler; import com.netflix.simianarmy.conformity.ConformityClusterTracker; import com.netflix.simianarmy.conformity.ConformityEmailNotifier; import com.netflix.simianarmy.conformity.ConformityMonkey; import com.netflix.simianarmy.conformity.ConformityRuleEngine; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** The basic implementation of Conformity Monkey. */ public class BasicConformityMonkey extends ConformityMonkey { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicConformityMonkey.class); /** The Constant NS. */ private static final String NS = "simianarmy.conformity."; /** The cfg. */ private final MonkeyConfiguration cfg; private final ClusterCrawler crawler; private final ConformityEmailNotifier emailNotifier; private final Collection regions = Lists.newArrayList(); private final ConformityClusterTracker clusterTracker; private final MonkeyCalendar calendar; private final ConformityRuleEngine ruleEngine; /** Flag to indicate whether the monkey is leashed. */ private boolean leashed; /** * Clusters that are not conforming in the last check. */ private final Map> nonconformingClusters = Maps.newHashMap(); /** * Clusters that are conforming in the last check. */ private final Map> conformingClusters = Maps.newHashMap(); /** * Clusters that the monkey failed to check for some reason. */ private final Map> failedClusters = Maps.newHashMap(); /** * Clusters that do not exist in the cloud anymore. */ private final Map> nonexistentClusters = Maps.newHashMap(); /** * Instantiates a new basic conformity monkey. * * @param ctx * the ctx */ public BasicConformityMonkey(Context ctx) { super(ctx); cfg = ctx.configuration(); crawler = ctx.clusterCrawler(); ruleEngine = ctx.ruleEngine(); emailNotifier = ctx.emailNotifier(); for (String region : ctx.regions()) { regions.add(region); } clusterTracker = ctx.clusterTracker(); calendar = ctx.calendar(); leashed = ctx.isLeashed(); } /** {@inheritDoc} */ @Override public void doMonkeyBusiness() { cfg.reload(); context().resetEventReport(); if (isConformityMonkeyEnabled()) { nonconformingClusters.clear(); conformingClusters.clear(); failedClusters.clear(); nonexistentClusters.clear(); List clusters = crawler.clusters(); Map> existingClusterNamesByRegion = Maps.newHashMap(); for (String region : regions) { existingClusterNamesByRegion.put(region, new HashSet()); } for (Cluster cluster : clusters) { existingClusterNamesByRegion.get(cluster.getRegion()).add(cluster.getName()); } List trackedClusters = clusterTracker.getAllClusters(regions.toArray(new String[regions.size()])); for (Cluster trackedCluster : trackedClusters) { if (!existingClusterNamesByRegion.get(trackedCluster.getRegion()).contains(trackedCluster.getName())) { addCluster(nonexistentClusters, trackedCluster); } } for (String region : regions) { Collection toDelete = nonexistentClusters.get(region); if (toDelete != null) { clusterTracker.deleteClusters(toDelete.toArray(new Cluster[toDelete.size()])); } } LOGGER.info(String.format("Performing conformity check for %d crawled clusters.", clusters.size())); Date now = calendar.now().getTime(); for (Cluster cluster : clusters) { boolean conforming; try { conforming = ruleEngine.check(cluster); } catch (Exception e) { LOGGER.error(String.format("Failed to perform conformity check for cluster %s", cluster.getName()), e); addCluster(failedClusters, cluster); continue; } cluster.setUpdateTime(now); cluster.setConforming(conforming); if (conforming) { LOGGER.info(String.format("Cluster %s is conforming", cluster.getName())); addCluster(conformingClusters, cluster); } else { LOGGER.info(String.format("Cluster %s is not conforming", cluster.getName())); addCluster(nonconformingClusters, cluster); } if (!leashed) { LOGGER.info(String.format("Saving cluster %s", cluster.getName())); clusterTracker.addOrUpdate(cluster); } else { LOGGER.info(String.format( "The conformity monkey is leashed, no data change is made for cluster %s.", cluster.getName())); } } if (!leashed) { emailNotifier.sendNotifications(); } else { LOGGER.info("Conformity monkey is leashed, no notification is sent."); } if (cfg.getBoolOrElse(NS + "summaryEmail.enabled", true)) { sendConformitySummaryEmail(); } } } private static void addCluster(Map> map, Cluster cluster) { Collection clusters = map.get(cluster.getRegion()); if (clusters == null) { clusters = Lists.newArrayList(); map.put(cluster.getRegion(), clusters); } clusters.add(cluster); } /** * Send a summary email with about the last run of the conformity monkey. */ protected void sendConformitySummaryEmail() { String summaryEmailTarget = cfg.getStr(NS + "summaryEmail.to"); if (!StringUtils.isEmpty(summaryEmailTarget)) { if (!emailNotifier.isValidEmail(summaryEmailTarget)) { LOGGER.error(String.format("The email target address '%s' for Conformity summary email is invalid", summaryEmailTarget)); return; } StringBuilder message = new StringBuilder(); for (String region : regions) { appendSummary(message, "nonconforming", nonconformingClusters, region, true); appendSummary(message, "failed to check", failedClusters, region, true); appendSummary(message, "nonexistent", nonexistentClusters, region, true); appendSummary(message, "conforming", conformingClusters, region, false); } String subject = getSummaryEmailSubject(); emailNotifier.sendEmail(summaryEmailTarget, subject, message.toString()); } } private void appendSummary(StringBuilder message, String summaryName, Map> regionToClusters, String region, boolean showDetails) { Collection clusters = regionToClusters.get(region); if (clusters == null) { clusters = Lists.newArrayList(); } message.append(String.format("Total %s clusters = %d in region %s
", summaryName, clusters.size(), region)); if (showDetails) { List clusterNames = Lists.newArrayList(); for (Cluster cluster : clusters) { clusterNames.add(cluster.getName()); } message.append(String.format("List: %s

", StringUtils.join(clusterNames, ","))); } } /** * Gets the summary email subject for the last run of conformity monkey. * @return the subject of the summary email */ protected String getSummaryEmailSubject() { return String.format("Conformity monkey execution summary (%s)", StringUtils.join(regions, ",")); } private boolean isConformityMonkeyEnabled() { String prop = NS + "enabled"; if (cfg.getBoolOrElse(prop, true)) { return true; } LOGGER.info("Conformity Monkey is disabled, set {}=true", prop); return false; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/conformity/BasicConformityMonkeyContext.java ================================================ /* * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.basic.conformity; import java.util.Collection; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.inject.Guice; import com.google.inject.Injector; import com.netflix.discovery.DiscoveryClient; import com.netflix.discovery.guice.EurekaModule; import com.netflix.simianarmy.aws.conformity.RDSConformityClusterTracker; import com.netflix.simianarmy.aws.conformity.SimpleDBConformityClusterTracker; import com.netflix.simianarmy.aws.conformity.crawler.AWSClusterCrawler; import com.netflix.simianarmy.aws.conformity.rule.BasicConformityEurekaClient; import com.netflix.simianarmy.aws.conformity.rule.ConformityEurekaClient; import com.netflix.simianarmy.aws.conformity.rule.CrossZoneLoadBalancing; import com.netflix.simianarmy.aws.conformity.rule.InstanceHasHealthCheckUrl; import com.netflix.simianarmy.aws.conformity.rule.InstanceHasStatusUrl; import com.netflix.simianarmy.aws.conformity.rule.InstanceInSecurityGroup; import com.netflix.simianarmy.aws.conformity.rule.InstanceInVPC; import com.netflix.simianarmy.aws.conformity.rule.InstanceIsHealthyInEureka; import com.netflix.simianarmy.aws.conformity.rule.InstanceTooOld; import com.netflix.simianarmy.aws.conformity.rule.SameZonesInElbAndAsg; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.ClusterCrawler; import com.netflix.simianarmy.conformity.ConformityClusterTracker; import com.netflix.simianarmy.conformity.ConformityEmailBuilder; import com.netflix.simianarmy.conformity.ConformityEmailNotifier; import com.netflix.simianarmy.conformity.ConformityMonkey; import com.netflix.simianarmy.conformity.ConformityRule; import com.netflix.simianarmy.conformity.ConformityRuleEngine; /** * The basic implementation of the context class for Conformity monkey. */ public class BasicConformityMonkeyContext extends BasicSimianArmyContext implements ConformityMonkey.Context { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicConformityMonkeyContext.class); /** The email notifier. */ private final ConformityEmailNotifier emailNotifier; private final ConformityClusterTracker clusterTracker; private final Collection regions; private final ClusterCrawler clusterCrawler; private final AmazonSimpleEmailServiceClient sesClient; private final ConformityEmailBuilder conformityEmailBuilder; private final String defaultEmail; private final String[] ccEmails; private final String sourceEmail; private final ConformityRuleEngine ruleEngine; private final boolean leashed; private final Map regionToAwsClient = Maps.newHashMap(); /** * The constructor. */ public BasicConformityMonkeyContext() { super("simianarmy.properties", "client.properties", "conformity.properties"); regions = Lists.newArrayList(region()); // By default, the monkey is leashed leashed = configuration().getBoolOrElse("simianarmy.conformity.leashed", true); LOGGER.info(String.format("Conformity Monkey is running in: %s", regions)); String sdbDomain = configuration().getStrOrElse("simianarmy.conformity.sdb.domain", "SIMIAN_ARMY"); String dbDriver = configuration().getStr("simianarmy.recorder.db.driver"); String dbUser = configuration().getStr("simianarmy.recorder.db.user"); String dbPass = configuration().getStr("simianarmy.recorder.db.pass"); String dbUrl = configuration().getStr("simianarmy.recorder.db.url"); String dbTable = configuration().getStr("simianarmy.conformity.resources.db.table"); if (dbDriver == null) { clusterTracker = new SimpleDBConformityClusterTracker(awsClient(), sdbDomain); } else { RDSConformityClusterTracker rdsClusterTracker = new RDSConformityClusterTracker(dbDriver, dbUser, dbPass, dbUrl, dbTable); rdsClusterTracker.init(); clusterTracker = rdsClusterTracker; } ruleEngine = new ConformityRuleEngine(); boolean eurekaEnabled = configuration().getBoolOrElse("simianarmy.conformity.Eureka.enabled", false); if (eurekaEnabled) { LOGGER.info("Initializing Discovery client."); Injector injector = Guice.createInjector(new EurekaModule()); DiscoveryClient discoveryClient = injector.getInstance(DiscoveryClient.class); ConformityEurekaClient conformityEurekaClient = new BasicConformityEurekaClient(discoveryClient); if (configuration().getBoolOrElse( "simianarmy.conformity.rule.InstanceIsHealthyInEureka.enabled", false)) { ruleEngine.addRule(new InstanceIsHealthyInEureka(conformityEurekaClient)); } if (configuration().getBoolOrElse( "simianarmy.conformity.rule.InstanceHasHealthCheckUrl.enabled", false)) { ruleEngine.addRule(new InstanceHasHealthCheckUrl(conformityEurekaClient)); } if (configuration().getBoolOrElse( "simianarmy.conformity.rule.InstanceHasStatusUrl.enabled", false)) { ruleEngine.addRule(new InstanceHasStatusUrl(conformityEurekaClient)); } } else { LOGGER.info("Discovery/Eureka is not enabled, the conformity rules that need Eureka are not added."); } if (configuration().getBoolOrElse( "simianarmy.conformity.rule.InstanceInSecurityGroup.enabled", false)) { String requiredSecurityGroups = configuration().getStr( "simianarmy.conformity.rule.InstanceInSecurityGroup.requiredSecurityGroups"); if (!StringUtils.isBlank(requiredSecurityGroups)) { ruleEngine.addRule(new InstanceInSecurityGroup(getAwsCredentialsProvider(), StringUtils.split(requiredSecurityGroups, ","))); } else { LOGGER.info("No required security groups is specified, " + "the conformity rule InstanceInSecurityGroup is ignored."); } } if (configuration().getBoolOrElse( "simianarmy.conformity.rule.InstanceTooOld.enabled", false)) { ruleEngine.addRule(new InstanceTooOld(getAwsCredentialsProvider(), (int) configuration().getNumOrElse( "simianarmy.conformity.rule.InstanceTooOld.instanceAgeThreshold", 180))); } if (configuration().getBoolOrElse( "simianarmy.conformity.rule.SameZonesInElbAndAsg.enabled", false)) { ruleEngine().addRule(new SameZonesInElbAndAsg(getAwsCredentialsProvider())); } if (configuration().getBoolOrElse( "simianarmy.conformity.rule.InstanceInVPC.enabled", false)) { ruleEngine.addRule(new InstanceInVPC(getAwsCredentialsProvider())); } if (configuration().getBoolOrElse( "simianarmy.conformity.rule.CrossZoneLoadBalancing.enabled", false)) { ruleEngine().addRule(new CrossZoneLoadBalancing(getAwsCredentialsProvider())); } createClient(region()); regionToAwsClient.put(region(), awsClient()); clusterCrawler = new AWSClusterCrawler(regionToAwsClient, configuration()); sesClient = new AmazonSimpleEmailServiceClient(); if (configuration().getStr("simianarmy.aws.email.region") != null) { sesClient.setRegion(Region.getRegion(Regions.fromName(configuration().getStr("simianarmy.aws.email.region")))); } defaultEmail = configuration().getStrOrElse("simianarmy.conformity.notification.defaultEmail", null); ccEmails = StringUtils.split( configuration().getStrOrElse("simianarmy.conformity.notification.ccEmails", ""), ","); sourceEmail = configuration().getStrOrElse("simianarmy.conformity.notification.sourceEmail", null); conformityEmailBuilder = new BasicConformityEmailBuilder(); emailNotifier = new ConformityEmailNotifier(getConformityEmailNotifierContext()); } public ConformityEmailNotifier.Context getConformityEmailNotifierContext() { return new ConformityEmailNotifier.Context() { @Override public AmazonSimpleEmailServiceClient sesClient() { return sesClient; } @Override public int openHour() { return (int) configuration().getNumOrElse("simianarmy.conformity.notification.openHour", 0); } @Override public int closeHour() { return (int) configuration().getNumOrElse("simianarmy.conformity.notification.closeHour", 24); } @Override public String defaultEmail() { return defaultEmail; } @Override public Collection regions() { return regions; } @Override public ConformityClusterTracker clusterTracker() { return clusterTracker; } @Override public ConformityEmailBuilder emailBuilder() { return conformityEmailBuilder; } @Override public String[] ccEmails() { return ccEmails; } @Override public Collection rules() { return ruleEngine.rules(); } @Override public String sourceEmail() { return sourceEmail; } }; } @Override public ClusterCrawler clusterCrawler() { return clusterCrawler; } @Override public ConformityRuleEngine ruleEngine() { return ruleEngine; } /** {@inheritDoc} */ @Override public ConformityEmailNotifier emailNotifier() { return emailNotifier; } @Override public Collection regions() { return regions; } @Override public boolean isLeashed() { return leashed; } @Override public ConformityClusterTracker clusterTracker() { return clusterTracker; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/janitor/BasicJanitorEmailBuilder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.janitor; import java.util.Collection; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.janitor.JanitorEmailBuilder; /** The basic implementation of the email builder for Janitor monkey. */ public class BasicJanitorEmailBuilder extends JanitorEmailBuilder { private static final String[] TABLE_COLUMNS = {"Resource Type", "Resource", "Region", "Description", "Expected Termination Time", "Termination Reason", "View/Edit"}; private static final String AHREF_TEMPLATE = "%s"; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("EEE, MMM dd, yyyy"); private Map> emailToResources; @Override public void setEmailToResources(Map> emailToResources) { Validate.notNull(emailToResources); this.emailToResources = emailToResources; } @Override protected String getHeader() { StringBuilder header = new StringBuilder(); header.append("

Janitor Notifications

"); header.append( "The following resource(s) have been marked for cleanup by Janitor monkey " + "as potential unused resources. This is a non-repeating notification.
"); return header.toString(); } @Override protected String getEntryTable(String emailAddress) { StringBuilder table = new StringBuilder(); table.append(getHtmlTableHeader(getTableColumns())); for (Resource resource : emailToResources.get(emailAddress)) { table.append(getResourceRow(resource)); } table.append(""); return table.toString(); } @Override protected String getFooter() { return "
Janitor Monkey wiki: https://github.com/Netflix/SimianArmy/wiki
"; } /** * Gets the url to view the details of the resource. * @param resource the resource * @return the url to view/edit the resource. */ protected String getResourceUrl(Resource resource) { return null; } /** * Gets the string when displaying the resource, e.g. the id. * @param resource the resource to display * @return the string to represent the resource */ protected String getResourceDisplay(Resource resource) { return resource.getId(); } /** * Gets the url to edit the Janitor termination of the resource. * @param resource the resource * @return the url to edit the Janitor termination the resource. */ protected String getJanitorResourceUrl(Resource resource) { return null; } /** Gets the table columns for the table in the email. * * @return the array of column names */ protected String[] getTableColumns() { return TABLE_COLUMNS; } /** * Gets the row for a resource in the table in the email body. * @param resource the resource to display * @return the table row in the email body */ protected String getResourceRow(Resource resource) { StringBuilder message = new StringBuilder(); message.append(""); message.append(getHtmlCell(resource.getResourceType().name())); String resourceUrl = getResourceUrl(resource); if (!StringUtils.isEmpty(resourceUrl)) { message.append(getHtmlCell(String.format(AHREF_TEMPLATE, resourceUrl, getResourceDisplay(resource)))); } else { message.append(getHtmlCell(getResourceDisplay(resource))); } message.append(getHtmlCell(resource.getRegion())); if (resource.getDescription() == null) { message.append(getHtmlCell("")); } else { message.append(getHtmlCell(resource.getDescription().replace(";", "
").replace(",", "
"))); } message.append(getHtmlCell(DATE_FORMATTER.print(resource.getExpectedTerminationTime().getTime()))); message.append(getHtmlCell(resource.getTerminationReason())); String janitorUrl = getJanitorResourceUrl(resource); if (!StringUtils.isEmpty(janitorUrl)) { message.append(getHtmlCell(String.format(AHREF_TEMPLATE, janitorUrl, "View/Extend"))); } else { message.append(getHtmlCell("")); } message.append(""); return message.toString(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/janitor/BasicJanitorMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.janitor; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import com.netflix.servo.annotations.DataSourceType; import com.netflix.servo.annotations.Monitor; import com.netflix.servo.monitor.Monitors; import com.netflix.simianarmy.*; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.janitor.AbstractJanitor; import com.netflix.simianarmy.janitor.JanitorEmailNotifier; import com.netflix.simianarmy.janitor.JanitorMonkey; import com.netflix.simianarmy.janitor.JanitorResourceTracker; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** The basic implementation of Janitor Monkey. */ public class BasicJanitorMonkey extends JanitorMonkey { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicJanitorMonkey.class); /** The Constant NS. */ private static final String NS = "simianarmy.janitor."; /** The cfg. */ private final MonkeyConfiguration cfg; private final List janitors; private final JanitorEmailNotifier emailNotifier; private final String region; private final String accountName; private final JanitorResourceTracker resourceTracker; private final MonkeyRecorder recorder; private final MonkeyCalendar calendar; /** Keep track of the number of monkey runs */ protected final AtomicLong monkeyRuns = new AtomicLong(0); /** Keep track of the number of monkey errors */ protected final AtomicLong monkeyErrors = new AtomicLong(0); /** Emit a servor signal to track the running monkey */ protected final AtomicLong monkeyRunning = new AtomicLong(0); /** * Instantiates a new basic janitor monkey. * * @param ctx * the ctx */ public BasicJanitorMonkey(Context ctx) { super(ctx); this.cfg = ctx.configuration(); janitors = ctx.janitors(); emailNotifier = ctx.emailNotifier(); region = ctx.region(); accountName = ctx.accountName(); resourceTracker = ctx.resourceTracker(); recorder = ctx.recorder(); calendar = ctx.calendar(); // register this janitor with servo Monitors.registerObject("simianarmy.janitor", this); } /** {@inheritDoc} */ @Override public void doMonkeyBusiness() { cfg.reload(); context().resetEventReport(); if (!isJanitorMonkeyEnabled()) { return; } else { LOGGER.info(String.format("Marking resources with %d janitors.", janitors.size())); monkeyRuns.incrementAndGet(); monkeyRunning.set(1); // prepare to run, this just resets the counts so monitoring is sane for (AbstractJanitor janitor : janitors) { janitor.prepareToRun(); } for (AbstractJanitor janitor : janitors) { LOGGER.info(String.format("Running %s janitor for region %s", janitor.getResourceType(), janitor.getRegion())); try { janitor.markResources(); } catch (Exception e) { monkeyErrors.incrementAndGet(); LOGGER.error(String.format("Got an exception while %s janitor was marking for region %s", janitor.getResourceType(), janitor.getRegion()), e); } LOGGER.info(String.format("Marked %d resources of type %s in the last run.", janitor.getMarkedResources().size(), janitor.getResourceType().name())); LOGGER.info(String.format("Unmarked %d resources of type %s in the last run.", janitor.getUnmarkedResources().size(), janitor.getResourceType())); } if (!cfg.getBoolOrElse("simianarmy.janitor.leashed", true)) { emailNotifier.sendNotifications(); } else { LOGGER.info("Janitor Monkey is leashed, no notification is sent."); } LOGGER.info(String.format("Cleaning resources with %d janitors.", janitors.size())); for (AbstractJanitor janitor : janitors) { try { janitor.cleanupResources(); } catch (Exception e) { monkeyErrors.incrementAndGet(); LOGGER.error(String.format("Got an exception while %s janitor was cleaning for region %s", janitor.getResourceType(), janitor.getRegion()), e); } LOGGER.info(String.format("Cleaned %d resources of type %s in the last run.", janitor.getCleanedResources().size(), janitor.getResourceType())); LOGGER.info(String.format("Failed to clean %d resources of type %s in the last run.", janitor.getFailedToCleanResources().size(), janitor.getResourceType())); } if (cfg.getBoolOrElse(NS + "summaryEmail.enabled", true)) { sendJanitorSummaryEmail(); } monkeyRunning.set(0); } } @Override public Event optInResource(String resourceId) { return optInOrOutResource(resourceId, true, region); } @Override public Event optOutResource(String resourceId) { return optInOrOutResource(resourceId, false, region); } @Override public Event optInResource(String resourceId, String resourceRegion) { return optInOrOutResource(resourceId, true, resourceRegion); } @Override public Event optOutResource(String resourceId, String resourceRegion) { return optInOrOutResource(resourceId, false, resourceRegion); } private Event optInOrOutResource(String resourceId, boolean optIn, String resourceRegion) { if (resourceRegion == null) { resourceRegion = region; } Resource resource = resourceTracker.getResource(resourceId, resourceRegion); if (resource == null) { return null; } EventTypes eventType = optIn ? EventTypes.OPT_IN_RESOURCE : EventTypes.OPT_OUT_RESOURCE; long timestamp = calendar.now().getTimeInMillis(); // The same resource can have multiple events, so we add the timestamp to the id. Event evt = recorder.newEvent(Type.JANITOR, eventType, resource, resourceId + "@" + timestamp); recorder.recordEvent(evt); resource.setOptOutOfJanitor(!optIn); resourceTracker.addOrUpdate(resource); return evt; } /** * Send a summary email with about the last run of the janitor monkey. */ protected void sendJanitorSummaryEmail() { String summaryEmailTarget = cfg.getStr(NS + "summaryEmail.to"); if (!StringUtils.isEmpty(summaryEmailTarget)) { if (!emailNotifier.isValidEmail(summaryEmailTarget)) { LOGGER.error(String.format("The email target address '%s' for Janitor summary email is invalid", summaryEmailTarget)); return; } StringBuilder message = new StringBuilder(); for (AbstractJanitor janitor : janitors) { ResourceType resourceType = janitor.getResourceType(); appendSummary(message, "markings", resourceType, janitor.getMarkedResources(), janitor.getRegion()); appendSummary(message, "unmarkings", resourceType, janitor.getUnmarkedResources(), janitor.getRegion()); appendSummary(message, "cleanups", resourceType, janitor.getCleanedResources(), janitor.getRegion()); appendSummary(message, "cleanup failures", resourceType, janitor.getFailedToCleanResources(), janitor.getRegion()); } String subject = getSummaryEmailSubject(); emailNotifier.sendEmail(summaryEmailTarget, subject, message.toString()); } } private void appendSummary(StringBuilder message, String summaryName, ResourceType resourceType, Collection resources, String janitorRegion) { message.append(String.format("Total %s for %s = %d in region %s
", summaryName, resourceType.name(), resources.size(), janitorRegion)); message.append(String.format("List: %s
", printResources(resources))); } private String printResources(Collection resources) { StringBuilder sb = new StringBuilder(); boolean isFirst = true; for (Resource r : resources) { if (!isFirst) { sb.append(","); } else { isFirst = false; } sb.append(r.getId()); } return sb.toString(); } /** * Gets the summary email subject for the last run of janitor monkey. * @return the subject of the summary email */ protected String getSummaryEmailSubject() { return String.format("Janitor monkey execution summary (%s, %s)", accountName, region); } /** * Handle cleanup error. This has been abstracted so subclasses can decide to continue causing chaos if desired. * * @param resource * the instance * @param e * the exception */ protected void handleCleanupError(Resource resource, Throwable e) { String msg = String.format("Failed to clean up %s resource %s with error %s", resource.getResourceType(), resource.getId(), e.getMessage()); LOGGER.error(msg); throw new RuntimeException(msg, e); } private boolean isJanitorMonkeyEnabled() { String prop = NS + "enabled"; if (cfg.getBoolOrElse(prop, true)) { return true; } LOGGER.info("JanitorMonkey disabled, set {}=true", prop); return false; } @Monitor(name="runs", type=DataSourceType.COUNTER) public long getMonkeyRuns() { return monkeyRuns.get(); } @Monitor(name="errors", type=DataSourceType.GAUGE) public long getMonkeyErrors() { return monkeyErrors.get(); } @Monitor(name="running", type=DataSourceType.GAUGE) public long getMonkeyRunning() { return monkeyRunning.get(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/janitor/BasicJanitorMonkeyContext.java ================================================ /* * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.basic.janitor; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.google.inject.Guice; import com.google.inject.Injector; import com.netflix.discovery.DiscoveryClient; import com.netflix.discovery.guice.EurekaModule; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.MonkeyRecorder; import com.netflix.simianarmy.aws.janitor.*; import com.netflix.simianarmy.aws.janitor.crawler.*; import com.netflix.simianarmy.aws.janitor.crawler.edda.*; import com.netflix.simianarmy.aws.janitor.rule.ami.UnusedImageRule; import com.netflix.simianarmy.aws.janitor.rule.asg.*; import com.netflix.simianarmy.aws.janitor.rule.elb.OrphanedELBRule; import com.netflix.simianarmy.aws.janitor.rule.generic.TagValueExclusionRule; import com.netflix.simianarmy.aws.janitor.rule.generic.UntaggedRule; import com.netflix.simianarmy.aws.janitor.rule.instance.OrphanedInstanceRule; import com.netflix.simianarmy.aws.janitor.rule.launchconfig.OldUnusedLaunchConfigRule; import com.netflix.simianarmy.aws.janitor.rule.snapshot.NoGeneratedAMIRule; import com.netflix.simianarmy.aws.janitor.rule.volume.DeleteOnTerminationRule; import com.netflix.simianarmy.aws.janitor.rule.volume.OldDetachedVolumeRule; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.edda.EddaClient; import com.netflix.simianarmy.janitor.*; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * The basic implementation of the context class for Janitor monkey. */ public class BasicJanitorMonkeyContext extends BasicSimianArmyContext implements JanitorMonkey.Context { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicJanitorMonkeyContext.class); /** The email notifier. */ private final JanitorEmailNotifier emailNotifier; private final JanitorResourceTracker janitorResourceTracker; /** The janitors. */ private final List janitors; private final String monkeyRegion; private final MonkeyCalendar monkeyCalendar; private final AmazonSimpleEmailServiceClient sesClient; private final JanitorEmailBuilder janitorEmailBuilder; private final String defaultEmail; private final String[] ccEmails; private final String sourceEmail; private final String ownerEmailDomain; private final int daysBeforeTermination; /** * The constructor. */ public BasicJanitorMonkeyContext() { super("simianarmy.properties", "client.properties", "janitor.properties"); monkeyRegion = region(); monkeyCalendar = calendar(); String resourceDomain = configuration().getStrOrElse("simianarmy.janitor.resources.sdb.domain", "SIMIAN_ARMY"); Set enabledResourceSet = getEnabledResourceSet(); String dbDriver = configuration().getStr("simianarmy.recorder.db.driver"); String dbUser = configuration().getStr("simianarmy.recorder.db.user"); String dbPass = configuration().getStr("simianarmy.recorder.db.pass"); String dbUrl = configuration().getStr("simianarmy.recorder.db.url"); String dbTable = configuration().getStr("simianarmy.janitor.resources.db.table"); if (dbDriver == null) { janitorResourceTracker = new SimpleDBJanitorResourceTracker(awsClient(), resourceDomain); } else { RDSJanitorResourceTracker rdsTracker = new RDSJanitorResourceTracker(dbDriver, dbUser, dbPass, dbUrl, dbTable); rdsTracker.init(); janitorResourceTracker = rdsTracker; } janitorEmailBuilder = new BasicJanitorEmailBuilder(); sesClient = new AmazonSimpleEmailServiceClient(); if (configuration().getStr("simianarmy.aws.email.region") != null) { sesClient.setRegion(Region.getRegion(Regions.fromName(configuration().getStr("simianarmy.aws.email.region")))); } defaultEmail = configuration().getStrOrElse("simianarmy.janitor.notification.defaultEmail", ""); ccEmails = StringUtils.split( configuration().getStrOrElse("simianarmy.janitor.notification.ccEmails", ""), ","); sourceEmail = configuration().getStrOrElse("simianarmy.janitor.notification.sourceEmail", ""); ownerEmailDomain = configuration().getStrOrElse("simianarmy.janitor.notification.ownerEmailDomain", ""); daysBeforeTermination = (int) configuration().getNumOrElse("simianarmy.janitor.notification.daysBeforeTermination", 3); emailNotifier = new JanitorEmailNotifier(getJanitorEmailNotifierContext()); janitors = new ArrayList(); if (enabledResourceSet.contains("ASG")) { janitors.add(getASGJanitor()); } if (enabledResourceSet.contains("INSTANCE")) { janitors.add(getInstanceJanitor()); } if (enabledResourceSet.contains("EBS_VOLUME")) { janitors.add(getEBSVolumeJanitor()); } if (enabledResourceSet.contains("EBS_SNAPSHOT")) { janitors.add(getEBSSnapshotJanitor()); } if (enabledResourceSet.contains("LAUNCH_CONFIG")) { janitors.add(getLaunchConfigJanitor()); } if (enabledResourceSet.contains("IMAGE")) { janitors.add(getImageJanitor()); } if (enabledResourceSet.contains("ELB")) { janitors.add(getELBJanitor()); } } protected JanitorRuleEngine createJanitorRuleEngine() { JanitorRuleEngine ruleEngine = new BasicJanitorRuleEngine(); if (configuration().getBoolOrElse("simianarmy.janitor.rule.TagValueExclusionRule.enabled", false)) { String tagsList = configuration().getStr("simianarmy.janitor.rule.TagValueExclusionRule.tags"); String valsList = configuration().getStr("simianarmy.janitor.rule.TagValueExclusionRule.vals"); if (tagsList != null && valsList != null) { TagValueExclusionRule rule = new TagValueExclusionRule(tagsList.split(","), valsList.split(",")); ruleEngine.addExclusionRule(rule); } } return ruleEngine; } private ASGJanitor getASGJanitor() { JanitorRuleEngine ruleEngine = createJanitorRuleEngine(); boolean discoveryEnabled = configuration().getBoolOrElse("simianarmy.janitor.Eureka.enabled", false); ASGInstanceValidator instanceValidator; if (discoveryEnabled) { LOGGER.info("Initializing Discovery client."); Injector injector = Guice.createInjector(new EurekaModule()); DiscoveryClient discoveryClient = injector.getInstance(DiscoveryClient.class); instanceValidator = new DiscoveryASGInstanceValidator(discoveryClient); } else { LOGGER.info("Discovery/Eureka is not enabled, use the dummy instance validator."); instanceValidator = new DummyASGInstanceValidator(); } if (configuration().getBoolOrElse("simianarmy.janitor.rule.oldEmptyASGRule.enabled", false)) { ruleEngine.addRule(new OldEmptyASGRule(monkeyCalendar, (int) configuration().getNumOrElse( "simianarmy.janitor.rule.oldEmptyASGRule.launchConfigAgeThreshold", 50), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.oldEmptyASGRule.retentionDays", 10), instanceValidator )); } if (configuration().getBoolOrElse("simianarmy.janitor.rule.suspendedASGRule.enabled", false)) { ruleEngine.addRule(new SuspendedASGRule(monkeyCalendar, (int) configuration().getNumOrElse( "simianarmy.janitor.rule.suspendedASGRule.suspensionAgeThreshold", 2), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.suspendedASGRule.retentionDays", 5), instanceValidator )); } if (configuration().getBoolOrElse("simianarmy.janitor.rule.untaggedRule.enabled", false) && getUntaggedRuleResourceSet().contains("ASG")) { ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet("simianarmy.janitor.rule.untaggedRule.requiredTags"), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner", 3), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner", 8))); } JanitorCrawler crawler; if (configuration().getBoolOrElse("simianarmy.janitor.edda.enabled", false)) { crawler = new EddaASGJanitorCrawler(createEddaClient(), awsClient().region()); } else { crawler = new ASGJanitorCrawler(awsClient()); } BasicJanitorContext asgJanitorCtx = new BasicJanitorContext( monkeyRegion, ruleEngine, crawler, janitorResourceTracker, monkeyCalendar, configuration(), recorder()); return new ASGJanitor(awsClient(), asgJanitorCtx); } private InstanceJanitor getInstanceJanitor() { JanitorRuleEngine ruleEngine = createJanitorRuleEngine(); if (configuration().getBoolOrElse("simianarmy.janitor.rule.orphanedInstanceRule.enabled", false)) { ruleEngine.addRule(new OrphanedInstanceRule(monkeyCalendar, (int) configuration().getNumOrElse( "simianarmy.janitor.rule.orphanedInstanceRule.instanceAgeThreshold", 2), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.orphanedInstanceRule.retentionDaysWithOwner", 3), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.orphanedInstanceRule.retentionDaysWithoutOwner", 8), configuration().getBoolOrElse( "simianarmy.janitor.rule.orphanedInstanceRule.opsworks.parentage", false))); } if (configuration().getBoolOrElse("simianarmy.janitor.rule.untaggedRule.enabled", false) && getUntaggedRuleResourceSet().contains("INSTANCE")) { ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet("simianarmy.janitor.rule.untaggedRule.requiredTags"), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner", 3), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner", 8))); } JanitorCrawler instanceCrawler; if (configuration().getBoolOrElse("simianarmy.janitor.edda.enabled", false)) { instanceCrawler = new EddaInstanceJanitorCrawler(createEddaClient(), awsClient().region()); } else { instanceCrawler = new InstanceJanitorCrawler(awsClient()); } BasicJanitorContext instanceJanitorCtx = new BasicJanitorContext( monkeyRegion, ruleEngine, instanceCrawler, janitorResourceTracker, monkeyCalendar, configuration(), recorder()); return new InstanceJanitor(awsClient(), instanceJanitorCtx); } private EBSVolumeJanitor getEBSVolumeJanitor() { JanitorRuleEngine ruleEngine = createJanitorRuleEngine(); if (configuration().getBoolOrElse("simianarmy.janitor.rule.oldDetachedVolumeRule.enabled", false)) { ruleEngine.addRule(new OldDetachedVolumeRule(monkeyCalendar, (int) configuration().getNumOrElse( "simianarmy.janitor.rule.oldDetachedVolumeRule.detachDaysThreshold", 30), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.oldDetachedVolumeRule.retentionDays", 7))); if (configuration().getBoolOrElse("simianarmy.janitor.edda.enabled", false) && configuration().getBoolOrElse("simianarmy.janitor.rule.deleteOnTerminationRule.enabled", false)) { ruleEngine.addRule(new DeleteOnTerminationRule(monkeyCalendar, (int) configuration().getNumOrElse( "simianarmy.janitor.rule.deleteOnTerminationRule.retentionDays", 3))); } } if (configuration().getBoolOrElse("simianarmy.janitor.rule.untaggedRule.enabled", false) && getUntaggedRuleResourceSet().contains("EBS_VOLUME")) { ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet("simianarmy.janitor.rule.untaggedRule.requiredTags"), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner", 3), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner", 8))); } JanitorCrawler volumeCrawler; if (configuration().getBoolOrElse("simianarmy.janitor.edda.enabled", false)) { volumeCrawler = new EddaEBSVolumeJanitorCrawler(createEddaClient(), awsClient().region()); } else { volumeCrawler = new EBSVolumeJanitorCrawler(awsClient()); } BasicJanitorContext volumeJanitorCtx = new BasicJanitorContext( monkeyRegion, ruleEngine, volumeCrawler, janitorResourceTracker, monkeyCalendar, configuration(), recorder()); return new EBSVolumeJanitor(awsClient(), volumeJanitorCtx); } private EBSSnapshotJanitor getEBSSnapshotJanitor() { JanitorRuleEngine ruleEngine = createJanitorRuleEngine(); if (configuration().getBoolOrElse("simianarmy.janitor.rule.noGeneratedAMIRule.enabled", false)) { ruleEngine.addRule(new NoGeneratedAMIRule(monkeyCalendar, (int) configuration().getNumOrElse("simianarmy.janitor.rule.noGeneratedAMIRule.ageThreshold", 30), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.noGeneratedAMIRule.retentionDays", 7), configuration().getStrOrElse( "simianarmy.janitor.rule.noGeneratedAMIRule.ownerEmail", null))); } if (configuration().getBoolOrElse("simianarmy.janitor.rule.untaggedRule.enabled", false) && getUntaggedRuleResourceSet().contains("EBS_SNAPSHOT")) { ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet("simianarmy.janitor.rule.untaggedRule.requiredTags"), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner", 3), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner", 8))); } JanitorCrawler snapshotCrawler; if (configuration().getBoolOrElse("simianarmy.janitor.edda.enabled", false)) { snapshotCrawler = new EddaEBSSnapshotJanitorCrawler( configuration().getStr("simianarmy.janitor.snapshots.ownerId"), createEddaClient(), awsClient().region()); } else { snapshotCrawler = new EBSSnapshotJanitorCrawler(awsClient()); } BasicJanitorContext snapshotJanitorCtx = new BasicJanitorContext( monkeyRegion, ruleEngine, snapshotCrawler, janitorResourceTracker, monkeyCalendar, configuration(), recorder()); return new EBSSnapshotJanitor(awsClient(), snapshotJanitorCtx); } private LaunchConfigJanitor getLaunchConfigJanitor() { JanitorRuleEngine ruleEngine = createJanitorRuleEngine(); if (configuration().getBoolOrElse("simianarmy.janitor.rule.oldUnusedLaunchConfigRule.enabled", false)) { ruleEngine.addRule(new OldUnusedLaunchConfigRule(monkeyCalendar, (int) configuration().getNumOrElse( "simianarmy.janitor.rule.oldUnusedLaunchConfigRule.ageThreshold", 4), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.oldUnusedLaunchConfigRule.retentionDays", 3))); } if (configuration().getBoolOrElse("simianarmy.janitor.rule.untaggedRule.enabled", false) && getUntaggedRuleResourceSet().contains("LAUNCH_CONFIG")) { ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet("simianarmy.janitor.rule.untaggedRule.requiredTags"), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner", 3), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner", 8))); } JanitorCrawler crawler; if (configuration().getBoolOrElse("simianarmy.janitor.edda.enabled", false)) { crawler = new EddaLaunchConfigJanitorCrawler( createEddaClient(), awsClient().region()); } else { crawler = new LaunchConfigJanitorCrawler(awsClient()); } BasicJanitorContext janitorCtx = new BasicJanitorContext( monkeyRegion, ruleEngine, crawler, janitorResourceTracker, monkeyCalendar, configuration(), recorder()); return new LaunchConfigJanitor(awsClient(), janitorCtx); } private ImageJanitor getImageJanitor() { JanitorCrawler crawler; if (configuration().getBoolOrElse("simianarmy.janitor.edda.enabled", false)) { crawler = new EddaImageJanitorCrawler(createEddaClient(), configuration().getStr("simianarmy.janitor.image.ownerId"), (int) configuration().getNumOrElse("simianarmy.janitor.image.crawler.lookBackDays", 60), awsClient().region()); } else { throw new RuntimeException("Image Janitor only works when Edda is enabled."); } JanitorRuleEngine ruleEngine = createJanitorRuleEngine(); if (configuration().getBoolOrElse("simianarmy.janitor.rule.unusedImageRule.enabled", false)) { ruleEngine.addRule(new UnusedImageRule(monkeyCalendar, (int) configuration().getNumOrElse( "simianarmy.janitor.rule.unusedImageRule.retentionDays", 3), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.unusedImageRule.lastReferenceDaysThreshold", 45))); } if (configuration().getBoolOrElse("simianarmy.janitor.rule.untaggedRule.enabled", false) && getUntaggedRuleResourceSet().contains("IMAGE")) { ruleEngine.addRule(new UntaggedRule(monkeyCalendar, getPropertySet("simianarmy.janitor.rule.untaggedRule.requiredTags"), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner", 3), (int) configuration().getNumOrElse( "simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner", 8))); } BasicJanitorContext janitorCtx = new BasicJanitorContext( monkeyRegion, ruleEngine, crawler, janitorResourceTracker, monkeyCalendar, configuration(), recorder()); return new ImageJanitor(awsClient(), janitorCtx); } private ELBJanitor getELBJanitor() { JanitorRuleEngine ruleEngine = createJanitorRuleEngine(); if (configuration().getBoolOrElse("simianarmy.janitor.rule.orphanedELBRule.enabled", false)) { ruleEngine.addRule(new OrphanedELBRule(monkeyCalendar, (int) configuration().getNumOrElse( "simianarmy.janitor.rule.orphanedELBRule.retentionDays", 7))); } JanitorCrawler elbCrawler; if (configuration().getBoolOrElse("simianarmy.janitor.edda.enabled", false)) { boolean useEddaApplicationOwner = configuration().getBoolOrElse("simianarmy.janitor.rule.orphanedELBRule.edda.useApplicationOwner", false); String eddaFallbackOwnerEmail = configuration().getStr("simianarmy.janitor.rule.orphanedELBRule.edda.fallbackOwnerEmail"); elbCrawler = new EddaELBJanitorCrawler(createEddaClient(), eddaFallbackOwnerEmail, useEddaApplicationOwner, awsClient().region()); } else { elbCrawler = new ELBJanitorCrawler(awsClient()); } BasicJanitorContext elbJanitorCtx = new BasicJanitorContext( monkeyRegion, ruleEngine, elbCrawler, janitorResourceTracker, monkeyCalendar, configuration(), recorder()); return new ELBJanitor(awsClient(), elbJanitorCtx); } private EddaClient createEddaClient() { return new EddaClient((int) configuration().getNumOrElse("simianarmy.janitor.edda.client.timeout", 30000), (int) configuration().getNumOrElse("simianarmy.janitor.edda.client.retries", 3), (int) configuration().getNumOrElse("simianarmy.janitor.edda.client.retryInterval", 1000), configuration()); } private Set getEnabledResourceSet() { Set enabledResourceSet = new HashSet(); String enabledResources = configuration().getStr("simianarmy.janitor.enabledResources"); if (StringUtils.isNotBlank(enabledResources)) { for (String resourceType : enabledResources.split(",")) { enabledResourceSet.add(resourceType.trim().toUpperCase()); } } return enabledResourceSet; } private Set getUntaggedRuleResourceSet() { Set untaggedRuleResourceSet = new HashSet(); if (configuration().getBoolOrElse("simianarmy.janitor.rule.untaggedRule.enabled", false)) { String untaggedRuleResources = configuration().getStr("simianarmy.janitor.rule.untaggedRule.resources"); if (StringUtils.isNotBlank(untaggedRuleResources)) { for (String resourceType : untaggedRuleResources.split(",")) { untaggedRuleResourceSet.add(resourceType.trim().toUpperCase()); } } } return untaggedRuleResourceSet; } private Set getPropertySet(String property) { Set propertyValueSet = new HashSet(); String propertyValue = configuration().getStr(property); if (StringUtils.isNotBlank(propertyValue)) { for (String propertyValueItem : propertyValue.split(",")) { propertyValueSet.add(propertyValueItem.trim()); } } return propertyValueSet; } public JanitorEmailNotifier.Context getJanitorEmailNotifierContext() { return new JanitorEmailNotifier.Context() { @Override public AmazonSimpleEmailServiceClient sesClient() { return sesClient; } @Override public String defaultEmail() { return defaultEmail; } @Override public int daysBeforeTermination() { return daysBeforeTermination; } @Override public String region() { return monkeyRegion; } @Override public JanitorResourceTracker resourceTracker() { return janitorResourceTracker; } @Override public JanitorEmailBuilder emailBuilder() { return janitorEmailBuilder; } @Override public MonkeyCalendar calendar() { return monkeyCalendar; } @Override public String[] ccEmails() { return ccEmails; } @Override public String sourceEmail() { return sourceEmail; } @Override public String ownerEmailDomain() { return ownerEmailDomain; } }; } /** {@inheritDoc} */ @Override public List janitors() { return janitors; } /** {@inheritDoc} */ @Override public JanitorEmailNotifier emailNotifier() { return emailNotifier; } @Override public JanitorResourceTracker resourceTracker() { return janitorResourceTracker; } /** The Context class for Janitor. */ public static class BasicJanitorContext implements AbstractJanitor.Context { private final String region; private final JanitorRuleEngine ruleEngine; private final JanitorCrawler crawler; private final JanitorResourceTracker resourceTracker; private final MonkeyCalendar calendar; private final MonkeyConfiguration config; private final MonkeyRecorder recorder; /** * Constructor. * @param region the region of the janitor * @param ruleEngine the rule engine used by the janitor * @param crawler the crawler used by the janitor * @param resourceTracker the resource tracker used by the janitor * @param calendar the calendar used by the janitor * @param config the monkey configuration used by the janitor */ public BasicJanitorContext(String region, JanitorRuleEngine ruleEngine, JanitorCrawler crawler, JanitorResourceTracker resourceTracker, MonkeyCalendar calendar, MonkeyConfiguration config, MonkeyRecorder recorder) { this.region = region; this.resourceTracker = resourceTracker; this.ruleEngine = ruleEngine; this.crawler = crawler; this.calendar = calendar; this.config = config; this.recorder = recorder; } @Override public String region() { return region; } @Override public MonkeyConfiguration configuration() { return config; } @Override public MonkeyCalendar calendar() { return calendar; } @Override public JanitorRuleEngine janitorRuleEngine() { return ruleEngine; } @Override public JanitorCrawler janitorCrawler() { return crawler; } @Override public JanitorResourceTracker janitorResourceTracker() { return resourceTracker; } @Override public MonkeyRecorder recorder() { return recorder; } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/janitor/BasicJanitorRuleEngine.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.janitor; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.janitor.JanitorRuleEngine; import com.netflix.simianarmy.janitor.Rule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Basic implementation of janitor rule engine that runs all containing rules to decide if a resource should be * a candidate of cleanup. */ public class BasicJanitorRuleEngine implements JanitorRuleEngine { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicJanitorRuleEngine.class); /** The rules to decide if a resource should be a candidate for cleanup. **/ private final List rules; /** The rules to decide if a resource should be excluded for cleanup. **/ private final List exclusionRules; /** * The constructor of JanitorRuleEngine. */ public BasicJanitorRuleEngine() { rules = new ArrayList(); exclusionRules = new ArrayList(); } /** * Decides whether the resource should be a candidate of cleanup based on the underlying rules. If any rule in the * rule set thinks the resource should be a candidate of cleanup, the method returns false which indicates that the * resource should be marked for cleanup. If multiple rules think the resource should be cleaned up, the rule with * the nearest expected termination time fills the termination reason and expected termination time. * * @param resource * The resource * @return true if the resource is valid and should not be a candidate of cleanup based on the underlying rules, * false otherwise. */ @Override public boolean isValid(Resource resource) { LOGGER.debug(String.format("Checking if resource %s of type %s is a cleanup candidate against %d rules and %d exclusion rules.", resource.getId(), resource.getResourceType(), rules.size(), exclusionRules.size())); for (Rule exclusionRule : exclusionRules) { if (exclusionRule.isValid(resource)) { LOGGER.info(String.format("Resource %s is not marked as a cleanup candidate because of an exclusion rule.", resource.getId())); return true; } } // We create a clone of the resource each time when we try the rule. In the first iteration of the rules // we identify the rule with the nearest termination date if there is any rule considers the resource // as a cleanup candidate. Then the rule is applied to the original resource. Rule nearestRule = null; if (rules.size() == 1) { nearestRule = rules.get(0); } else { Date nearestTerminationTime = null; for (Rule rule : rules) { Resource clone = resource.cloneResource(); if (!rule.isValid(clone)) { if (clone.getExpectedTerminationTime() != null) { if (nearestTerminationTime == null || nearestTerminationTime.after(clone.getExpectedTerminationTime())) { nearestRule = rule; nearestTerminationTime = clone.getExpectedTerminationTime(); } } } } } if (nearestRule != null && !nearestRule.isValid(resource)) { LOGGER.info(String.format("Resource %s is marked as a cleanup candidate.", resource.getId())); return false; } else { LOGGER.info(String.format("Resource %s is not marked as a cleanup candidate.", resource.getId())); return true; } } /** {@inheritDoc} */ @Override public BasicJanitorRuleEngine addRule(Rule rule) { rules.add(rule); return this; } /** {@inheritDoc} */ @Override public BasicJanitorRuleEngine addExclusionRule(Rule rule){ exclusionRules.add(rule); return this; } /** {@inheritDoc} */ @Override public List getRules() { return this.rules; } /** {@inheritDoc} */ @Override public List getExclusionRules() { return this.exclusionRules; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/basic/janitor/BasicVolumeTaggingMonkeyContext.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.basic.janitor; import com.google.common.collect.Lists; import com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey; import com.netflix.simianarmy.basic.BasicSimianArmyContext; import com.netflix.simianarmy.client.aws.AWSClient; import org.apache.commons.lang.StringUtils; import java.util.Collection; /** The basic context for the monkey that tags volumes with Janitor meta data. */ public class BasicVolumeTaggingMonkeyContext extends BasicSimianArmyContext implements VolumeTaggingMonkey.Context { private final Collection awsClients = Lists.newArrayList(); /** * The constructor. */ public BasicVolumeTaggingMonkeyContext() { super("simianarmy.properties", "client.properties", "volumeTagging.properties"); for (String r : StringUtils.split(region(), ",")) { createClient(r); awsClients.add(awsClient()); } } @Override public Collection awsClients() { return awsClients; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/BlockAllNetworkTrafficChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import com.netflix.simianarmy.CloudClient; import com.netflix.simianarmy.MonkeyConfiguration; /** * Blocks network traffic to/from instance, so it is running but offline. * * We actually put the instance into a different security group. First, because AWS requires a SG for some reason. * Second, because you might well want to continue to allow e.g. SSH inbound. */ public class BlockAllNetworkTrafficChaosType extends ChaosType { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BlockAllNetworkTrafficChaosType.class); private final String blockedSecurityGroupName; /** * Constructor. * * @param config * Configuration to use */ public BlockAllNetworkTrafficChaosType(MonkeyConfiguration config) { super(config, "BlockAllNetworkTraffic"); this.blockedSecurityGroupName = config.getStrOrElse(getConfigurationPrefix() + "group", "blocked-network"); } /** * We can apply the strategy iff the blocked security group is configured. */ @Override public boolean canApply(ChaosInstance instance) { CloudClient cloudClient = instance.getCloudClient(); String instanceId = instance.getInstanceId(); if (!cloudClient.canChangeInstanceSecurityGroups(instanceId)) { LOGGER.info("Not a VPC instance, can't change security groups"); return false; } return super.canApply(instance); } /** * Takes the instance off the network. */ @Override public void apply(ChaosInstance instance) { CloudClient cloudClient = instance.getCloudClient(); String instanceId = instance.getInstanceId(); if (!cloudClient.canChangeInstanceSecurityGroups(instanceId)) { throw new IllegalStateException("canApply should have returned false"); } String groupId = cloudClient.findSecurityGroup(instance.getInstanceId(), blockedSecurityGroupName); if (groupId == null) { LOGGER.info("Auto-creating security group {}", blockedSecurityGroupName); String description = "Empty security group for blocked instances"; groupId = cloudClient.createSecurityGroup(instance.getInstanceId(), blockedSecurityGroupName, description); } LOGGER.info("Blocking network traffic by applying security group {} to instance {}", groupId, instanceId); List groups = Lists.newArrayList(); groups.add(groupId); cloudClient.setInstanceSecurityGroups(instanceId, groups); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/BurnCpuChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Executes a CPU intensive program on the node, using up all available CPU. * * This simulates either a noisy CPU neighbor on the box or just a general issue with the CPU. */ public class BurnCpuChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public BurnCpuChaosType(MonkeyConfiguration config) { super(config, "BurnCpu"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/BurnIoChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.MonkeyConfiguration; /** * Executes a disk I/O intensive program on the node, reducing I/O capacity. * * This simulates either a noisy neighbor on the box or just a general issue with the disk. */ public class BurnIoChaosType extends ScriptChaosType { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BurnIoChaosType.class); /** * Enhancement: It would be nice to target other devices than the root disk. * * Considerations: * 1) EBS activity costs money. * 2) The root may be on EBS anyway. * 3) If it's costing money, we might want to stop after a while to stop runaway charges. * * coryb suggested this, and proposed something like this: * * tmp=$(mktemp) * df -hl -x tmpfs | awk '/\//{print $6}' > $tmp * mount=$(sed -n $((RANDOM%$(wc -l < $tmp)+1))p $tmp) * rm $tmp * * And then of=$mount/burn * * An alternative might be to run df over SSH, parse it here, and then pass the desired * path to the script. This keeps the script simpler. I don't think there's an easy way * to tell the difference between an EBS volume and an instance volume other than from the * EC2 API. */ /** * Constructor. * * @param config * Configuration to use */ public BurnIoChaosType(MonkeyConfiguration config) { super(config, "BurnIO"); } @Override public boolean canApply(ChaosInstance instance) { if (!super.canApply(instance)) { return false; } if (isRootVolumeEbs(instance) && !isBurnMoneyEnabled()) { LOGGER.debug("Root volume is EBS so BurnIO would cost money; skipping"); return false; } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/ChaosCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import java.util.EnumSet; import java.util.List; import com.amazonaws.services.autoscaling.model.TagDescription; import com.netflix.simianarmy.GroupType; /** * The Interface ChaosCrawler. */ public interface ChaosCrawler { /** * The Interface InstanceGroup. */ public interface InstanceGroup { /** * Type. * * @return the group type enum */ GroupType type(); /** * Name. * * @return the group string */ String name(); /** * Region. * * @return the region the group exists in */ String region(); /** * Tags. * * @return the list of tags associated with group type */ List tags(); /** * Instances. * * @return the list of instances */ List instances(); /** * Adds the instance. * * @param instance * the instance */ void addInstance(String instance); /** * Copies the Instance group replacing its name with * the supplied name. * * * @param name * @return the new instance group */ InstanceGroup copyAs(String name); } /** * Group types. * * @return the type of groups this crawler creates \set */ EnumSet groupTypes(); /** * Groups. * * @return the list */ List groups(); /** * Gets the up to date information for a collection of group names. * * @param names * the group names * @return the list of instance groups */ List groups(String... names); } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/ChaosEmailNotifier.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.netflix.simianarmy.aws.AWSEmailNotifier; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; /** The email notifier for Chaos monkey. * */ public abstract class ChaosEmailNotifier extends AWSEmailNotifier { /** Constructor. Currently the notifier is fixed the email client to * Amazon Simple Email Service. We can release this restriction when * we want to support different email clients. * * @param sesClient the AWS simple email service client. */ public ChaosEmailNotifier(AmazonSimpleEmailServiceClient sesClient) { super(sesClient); } /** * Sends an email notification for a termination of instance to group * owner's email address. * @param group the instance group * @param instance the instance id * @param chaosType the chosen chaos strategy */ public abstract void sendTerminationNotification(InstanceGroup group, String instance, ChaosType chaosType); /** * Sends an email notification for a termination of instance to a global * email address. * @param group the instance group * @param instance the instance id * @param chaosType the chosen chaos strategy */ public abstract void sendTerminationGlobalNotification(InstanceGroup group, String instance, ChaosType chaosType); } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/ChaosInstance.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import org.jclouds.domain.LoginCredentials; import org.jclouds.ssh.SshClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.CloudClient; /** * Wrapper around an instance on which we are going to cause chaos. */ public class ChaosInstance { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ChaosInstance.class); private final CloudClient cloudClient; private final String instanceId; private final SshConfig sshConfig; /** * Constructor. * * @param cloudClient * client for cloud access * @param instanceId * id of instance on cloud * @param sshConfig * SSH configuration to access instance */ public ChaosInstance(CloudClient cloudClient, String instanceId, SshConfig sshConfig) { this.cloudClient = cloudClient; this.instanceId = instanceId; this.sshConfig = sshConfig; } /** * Gets the {@link SshConfig} used to SSH to the instance. * * @return the {@link SshConfig} */ public SshConfig getSshConfig() { return sshConfig; } /** * Gets the {@link CloudClient} used to access the cloud. * * @return the {@link CloudClient} */ public CloudClient getCloudClient() { return cloudClient; } /** * Returns the instance id to identify the instance to the cloud client. * * @return instance id */ public String getInstanceId() { return instanceId; } /** * Memoize canConnectSsh function. */ private Boolean canConnectSsh = null; /** * Check if the SSH credentials are working. * * This is cached for the duration of this object. * * @return true iff ssh is configured and able to log on to instance. */ public boolean canConnectSsh(ChaosInstance instance) { if (!sshConfig.isEnabled()) { return false; } if (canConnectSsh == null) { try { // It would be nicer to keep this connection open, but then we'd have to be closed. SshClient client = connectSsh(); client.disconnect(); canConnectSsh = true; } catch (Exception e) { LOGGER.warn("Error making SSH connection to instance", e); canConnectSsh = false; } } return canConnectSsh; } /** * Connect to the instance over SSH. * * @return {@link SshClient} for connection */ public SshClient connectSsh() { if (!sshConfig.isEnabled()) { throw new IllegalStateException(); } LoginCredentials credentials = sshConfig.getCredentials(); SshClient ssh = cloudClient.connectSsh(instanceId, credentials); return ssh; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/ChaosInstanceSelector.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import java.util.Collection; /** * The Interface ChaosInstanceSelector. */ public interface ChaosInstanceSelector { /** * Select. Pick random instances out of the group with provided probability. Chaos will draw a random number and if * that random number is lower than probability then it will proceed to select an instance (at random) out of the * group. If the random number is higher than the provided probability then no instance will be selected and * null will be returned. * * When the probability value is bigger than 1, say N + 0.x, it will first applies the algorithm described above * with the probability value as 0.x to select possibly one instance, then it will randomly pick N instances. * * The probability is the run probability. If Chaos is running hourly between 9am and 3pm with an overall configured * probability of "1.0" then the probability provided to this routine would be 1.0/6 (6 hours in 9am-3pm). So the * typical probability here would be .1666. For Chaos to select an instance it will pick a random number between 0 * and 1. If that random number is less than the .1666 it will proceed to select an instance and return it, * otherwise it will return null. Over 6 runs it is likely that the random number be less than .1666, but it is not * certain. * * To make Chaos select an instance with 100% certainty it would have to be configured to run only once a day and * the instance group would have to be configured for "1.0" daily probability. * * @param group * the group * @param probability * the probability per run that an instance should be terminated. * @return the instance */ Collection select(InstanceGroup group, double probability); } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/ChaosMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import java.util.Date; import java.util.List; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.FeatureNotEnabledException; import com.netflix.simianarmy.InstanceGroupNotFoundException; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.MonkeyType; /** * The Class ChaosMonkey. */ public abstract class ChaosMonkey extends Monkey { /** * The Interface Context. */ public interface Context extends Monkey.Context { /** * Configuration. * * @return the monkey configuration */ MonkeyConfiguration configuration(); /** * Chaos crawler. * * @return the chaos crawler */ ChaosCrawler chaosCrawler(); /** * Chaos instance selector. * * @return the chaos instance selector */ ChaosInstanceSelector chaosInstanceSelector(); /** * Chaos email notifier. * * @return the chaos email notifier */ ChaosEmailNotifier chaosEmailNotifier(); } /** The context. */ private final Context ctx; /** * Instantiates a new chaos monkey. * * @param ctx * the context. */ public ChaosMonkey(Context ctx) { super(ctx); this.ctx = ctx; } /** * The monkey Type. */ public enum Type implements MonkeyType { /** chaos monkey. */ CHAOS } /** * The event types that this monkey causes. */ public enum EventTypes implements EventType { /** The chaos termination. */ CHAOS_TERMINATION, CHAOS_TERMINATION_SKIPPED } /** {@inheritDoc} */ @Override public final Type type() { return Type.CHAOS; } /** {@inheritDoc} */ @Override public Context context() { return ctx; } /** {@inheritDoc} */ @Override public abstract void doMonkeyBusiness(); /** * Gets the count of terminations since a specific time. Chaos should probably not continue to beat up an instance * group if the count exceeds a threshold. * * @param group * the group * @return true, if successful */ public abstract int getPreviousTerminationCount(ChaosCrawler.InstanceGroup group, Date after); /** * Record termination. This is used to notify system owners of terminations and to record terminations so that Chaos * does not continue to thrash the instance groups on later runs. * * @param group * the group * @param instance * the instance * @return the termination event */ public abstract Event recordTermination(ChaosCrawler.InstanceGroup group, String instance, ChaosType chaosType); /** * Terminates one instance right away from an instance group when there are available instances. * @param type * the type of the instance group * @param name * the name of the instance group * @return the termination event * @throws FeatureNotEnabledException * @throws InstanceGroupNotFoundException */ public abstract Event terminateNow(String type, String name, ChaosType chaosType) throws FeatureNotEnabledException, InstanceGroupNotFoundException; /** * Sends notification for the termination to the instance owners. * * @param group * the group * @param instance * the instance * @param chaosType * the chaos monkey strategy that was chosen */ public abstract void sendTerminationNotification(ChaosCrawler.InstanceGroup group, String instance, ChaosType chaosType); /** * Gets a list of all enabled chaos types for this ChaosMonkey. */ public abstract List getChaosTypes(); } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/ChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.CloudClient; import com.netflix.simianarmy.MonkeyConfiguration; /** * A strategy pattern for different types of chaos the chaos monkey can cause. */ public abstract class ChaosType { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ChaosType.class); /** * Configuration for this chaos type. */ private final MonkeyConfiguration config; /** * The unique key for the ChaosType. */ private final String key; /** * Is this strategy enabled? */ private final boolean enabled; /** * Protected constructor (abstract class). * * @param config * Configuration to use * @param key * Unique key for the ChaosType strategy */ protected ChaosType(MonkeyConfiguration config, String key) { this.config = config; this.key = key; this.enabled = config.getBoolOrElse(getConfigurationPrefix() + "enabled", getEnabledDefault()); LOGGER.info("ChaosType: {}: enabled={}", key, enabled); } /** * If not specified, controls whether we default to enabled. * * Most ChaosTypes should be disabled by default, not least for legacy compatibility, but we want at least one * strategy to be available. */ protected boolean getEnabledDefault() { return false; } /** * Returns the configuration key prefix to use for this strategy. */ protected String getConfigurationPrefix() { return "simianarmy.chaos." + key.toLowerCase() + "."; } /** * Returns the unique key for the ChaosType. */ public String getKey() { return key; } /** * Checks if this chaos type can be applied to the given instance. * * For example, if the strategy was to detach all the EBS volumes, that only makes sense if there are EBS volumes to * detach. */ public boolean canApply(ChaosInstance instance) { return isEnabled(); } /** * Returns whether we are enabled. */ public boolean isEnabled() { return enabled; } /** * Applies this chaos type to the specified instance. */ public abstract void apply(ChaosInstance instance); /** * Returns the ChaosType with the matching key. */ public static ChaosType parse(List all, String chaosTypeName) { for (ChaosType chaosType : all) { if (chaosType.getKey().equalsIgnoreCase(chaosTypeName)) { return chaosType; } } throw new IllegalArgumentException("Unknown chaos type value: " + chaosTypeName); } /** * Returns whether chaos types that cost money are allowed. */ protected boolean isBurnMoneyEnabled() { return config.getBoolOrElse("simianarmy.chaos.burnmoney", false); } /** * Checks whether the root volume of the specified instance is on EBS. * * @param instance id of instance * @return true iff root is on EBS */ protected boolean isRootVolumeEbs(ChaosInstance instance) { CloudClient cloudClient = instance.getCloudClient(); String instanceId = instance.getInstanceId(); List withRoot = cloudClient.listAttachedVolumes(instanceId, true); List withoutRoot = cloudClient.listAttachedVolumes(instanceId, false); return (withRoot.size() != withoutRoot.size()); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/DetachVolumesChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.CloudClient; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.basic.chaos.BasicChaosMonkey; /** * We force-detach all the EBS volumes. * * This is supposed to simulate a catastrophic failure of EBS, however the instance will (possibly) still keep running; * e.g. it should continue to respond to pings. */ public class DetachVolumesChaosType extends ChaosType { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(BasicChaosMonkey.class); /** * Constructor. * * @param config * Configuration to use */ public DetachVolumesChaosType(MonkeyConfiguration config) { super(config, "DetachVolumes"); } /** * Strategy can be applied iff there are any EBS volumes attached. */ @Override public boolean canApply(ChaosInstance instance) { CloudClient cloudClient = instance.getCloudClient(); String instanceId = instance.getInstanceId(); List volumes = cloudClient.listAttachedVolumes(instanceId, false); if (volumes.isEmpty()) { LOGGER.debug("Can't apply strategy: no non-root EBS volumes"); return false; } return super.canApply(instance); } /** * Force-detaches all attached EBS volumes from the instance. */ @Override public void apply(ChaosInstance instance) { CloudClient cloudClient = instance.getCloudClient(); String instanceId = instance.getInstanceId(); // IDEA: We could have a strategy where we detach some of the volumes... boolean force = true; for (String volumeId : cloudClient.listAttachedVolumes(instanceId, false)) { cloudClient.detachVolume(instanceId, volumeId, force); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/FailDnsChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Blocks TCP and UDP port 53, so DNS resolution fails. */ public class FailDnsChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public FailDnsChaosType(MonkeyConfiguration config) { super(config, "FailDns"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/FailDynamoDbChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Adds entries to /etc/hosts so that DynamoDB API endpoints are unreachable. */ public class FailDynamoDbChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public FailDynamoDbChaosType(MonkeyConfiguration config) { super(config, "FailDynamoDb"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/FailEc2ChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Adds entries to /etc/hosts so that EC2 API endpoints are unreachable. */ public class FailEc2ChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public FailEc2ChaosType(MonkeyConfiguration config) { super(config, "FailEc2"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/FailS3ChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Adds entries to /etc/hosts so that S3 API endpoints are unreachable. */ public class FailS3ChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public FailS3ChaosType(MonkeyConfiguration config) { super(config, "FailS3"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/FillDiskChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.MonkeyConfiguration; /** * Creates a huge file on the root device so that the disk fills up. */ public class FillDiskChaosType extends ScriptChaosType { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(FillDiskChaosType.class); /** * Enhancement: As with BurnIoChaosType, it would be nice to randomize the volume. * * coryb suggested this, and proposed this script: * * nohup dd if=/dev/urandom of=/burn bs=1M count=$(df -ml /burn | awk '/\//{print $2}') iflag=fullblock & */ /** * Constructor. * * @param config * Configuration to use */ public FillDiskChaosType(MonkeyConfiguration config) { super(config, "FillDisk"); } @Override public boolean canApply(ChaosInstance instance) { if (!super.canApply(instance)) { return false; } if (isRootVolumeEbs(instance) && !isBurnMoneyEnabled()) { LOGGER.debug("Root volume is EBS so FillDisk would cost money; skipping"); return false; } return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/KillProcessesChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Kills processes on the node. * * This simulates the process crashing (for any reason). */ public class KillProcessesChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public KillProcessesChaosType(MonkeyConfiguration config) { super(config, "KillProcesses"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/NetworkCorruptionChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Introduces network packet corruption using traffic-shaping. */ public class NetworkCorruptionChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public NetworkCorruptionChaosType(MonkeyConfiguration config) { super(config, "NetworkCorruption"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/NetworkLatencyChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Introduces network latency using traffic-shaping. */ public class NetworkLatencyChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public NetworkLatencyChaosType(MonkeyConfiguration config) { super(config, "NetworkLatency"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/NetworkLossChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Introduces network packet loss using traffic-shaping. */ public class NetworkLossChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public NetworkLossChaosType(MonkeyConfiguration config) { super(config, "NetworkLoss"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/NullRouteChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.MonkeyConfiguration; /** * Null routes the network, taking a node going offline. * * Currently we offline 10.x.x.x (the AWS private network range). * * I think the machine will still be publicly accessible, but won't be able to communicate with any other nodes on * the EC2 network. */ public class NullRouteChaosType extends ScriptChaosType { /** * Constructor. * * @param config * Configuration to use */ public NullRouteChaosType(MonkeyConfiguration config) { super(config, "NullRoute"); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/ScriptChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import java.io.IOException; import java.net.URL; import org.jclouds.compute.domain.ExecResponse; import org.jclouds.ssh.SshClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.netflix.simianarmy.MonkeyConfiguration; /** * Base class for chaos types that run a script over JClouds/SSH on the node. */ public abstract class ScriptChaosType extends ChaosType { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ScriptChaosType.class); /** * Constructor. * * @param config * Configuration to use * @param key * Key for the chaos money */ public ScriptChaosType(MonkeyConfiguration config, String key) { super(config, key); } /** * We can apply the strategy iff we can SSH to the instance. */ @Override public boolean canApply(ChaosInstance instance) { if (!instance.getSshConfig().isEnabled()) { LOGGER.info("Strategy disabled because SSH credentials not set"); return false; } if (!instance.canConnectSsh(instance)) { LOGGER.warn("Strategy disabled because SSH credentials failed"); return false; } return super.canApply(instance); } /** * Runs the script. */ @Override public void apply(ChaosInstance instance) { LOGGER.info("Running script for {} on instance {}", getKey(), instance.getInstanceId()); SshClient ssh = instance.connectSsh(); String filename = getKey().toLowerCase() + ".sh"; URL url = Resources.getResource(ScriptChaosType.class, "/scripts/" + filename); String script; try { script = Resources.toString(url, Charsets.UTF_8); } catch (IOException e) { throw new IllegalStateException("Error reading script resource", e); } ssh.put("/tmp/" + filename, script); ExecResponse response = ssh.exec("/bin/bash /tmp/" + filename); if (response.getExitStatus() != 0) { LOGGER.warn("Got non-zero output from running script: {}", response); } ssh.disconnect(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/ShutdownInstanceChaosType.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import com.netflix.simianarmy.CloudClient; import com.netflix.simianarmy.MonkeyConfiguration; /** * Shuts down the instance using the cloud instance-termination API. * * This is the classic chaos-monkey strategy. */ public class ShutdownInstanceChaosType extends ChaosType { /** * Constructor. * * @param config * Configuration to use */ public ShutdownInstanceChaosType(MonkeyConfiguration config) { super(config, "ShutdownInstance"); } /** * Shuts down the instance. */ @Override public void apply(ChaosInstance instance) { CloudClient cloudClient = instance.getCloudClient(); String instanceId = instance.getInstanceId(); cloudClient.terminateInstance(instanceId); } /** * We want to default to enabled. */ @Override protected boolean getEnabledDefault() { return true; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/chaos/SshConfig.java ================================================ /* * * Copyright 2013 Justin Santa Barbara. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.chaos; import java.io.File; import java.io.IOException; import org.jclouds.domain.LoginCredentials; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.google.common.io.Files; import com.netflix.simianarmy.MonkeyConfiguration; /** * Holds SSH connection info, used for script-based chaos types. */ public class SshConfig { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(SshConfig.class); /** * The SSH credentials to log on to an instance. */ private final LoginCredentials sshCredentials; /** * Constructor. * * @param config * Configuration to use */ public SshConfig(MonkeyConfiguration config) { String sshUser = config.getStrOrElse("simianarmy.chaos.ssh.user", "root"); String privateKey = null; String sshKeyPath = config.getStrOrElse("simianarmy.chaos.ssh.key", null); if (sshKeyPath != null) { sshKeyPath = sshKeyPath.trim(); if (sshKeyPath.startsWith("~/")) { String home = System.getProperty("user.home"); if (!Strings.isNullOrEmpty(home)) { if (!home.endsWith("/")) { home += "/"; } sshKeyPath = home + sshKeyPath.substring(2); } } LOGGER.debug("Reading SSH key from {}", sshKeyPath); try { privateKey = Files.toString(new File(sshKeyPath), Charsets.UTF_8); } catch (IOException e) { throw new IllegalStateException("Unable to read the specified SSH key: " + sshKeyPath, e); } } if (privateKey == null) { this.sshCredentials = LoginCredentials.builder().user(sshUser).build(); } else { this.sshCredentials = LoginCredentials.builder().user(sshUser).privateKey(privateKey).build(); } } /** * Get the configured SSH credentials. * * @return configured SSH credentials */ public LoginCredentials getCredentials() { return sshCredentials; } /** * Check if ssh is configured. * * @return true if credentials are configured */ public boolean isEnabled() { return sshCredentials != null; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/MonkeyRestClient.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.client; import org.apache.commons.lang.Validate; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy; import org.apache.http.impl.client.HttpClientBuilder; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; /** * A REST client used by monkeys. */ public abstract class MonkeyRestClient { private static final Logger LOGGER = LoggerFactory.getLogger(MonkeyRestClient.class); private final HttpClient httpClient; /** * Constructor. * @param timeout the timeout in milliseconds * @param maxRetries the max number of retries * @param retryInterval the interval in milliseconds between retries */ public MonkeyRestClient(int timeout, int maxRetries, int retryInterval) { Validate.isTrue(timeout >= 0); Validate.isTrue(maxRetries >= 0); Validate.isTrue(retryInterval > 0); RequestConfig config = RequestConfig.custom() .setConnectTimeout(timeout) .build(); httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(config) .setServiceUnavailableRetryStrategy(new DefaultServiceUnavailableRetryStrategy(maxRetries, retryInterval)) .build(); } /** * Gets the response in JSON from a url. * @param url the url * @return the JSON node for the response */ // CHECKSTYLE IGNORE MagicNumberCheck public JsonNode getJsonNodeFromUrl(String url) throws IOException { LOGGER.info(String.format("Getting Json response from url: %s", url)); HttpGet request = new HttpGet(url); request.setHeader("Accept", "application/json"); HttpResponse response = httpClient.execute(request); InputStream is = response.getEntity().getContent(); String jsonContent; if (is != null) { Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); jsonContent = s.hasNext() ? s.next() : ""; is.close(); } else { return null; } int code = response.getStatusLine().getStatusCode(); if (code == 404) { return null; } else if (code >= 300 || code < 200) { throw new DataReadException(code, url, jsonContent); } JsonNode result; try { ObjectMapper mapper = new ObjectMapper(); result = mapper.readTree(jsonContent); } catch (Exception e) { throw new RuntimeException(String.format("Error trying to parse json response from url %s, got: %s", url, jsonContent), e); } return result; } /** * Gets the base url of the service for a specific region. * @param region the region * @return the base url in the region */ public abstract String getBaseUrl(String region); public static class DataReadException extends RuntimeException { public DataReadException(int code, String url, String jsonContent) { super(String.format("Response code %d from url %s: %s", code, url, jsonContent)); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/aws/AWSClient.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.client.aws; import com.amazonaws.AmazonServiceException; import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSSessionCredentials; import com.amazonaws.services.autoscaling.AmazonAutoScalingClient; import com.amazonaws.services.autoscaling.model.*; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.AmazonEC2Client; import com.amazonaws.services.ec2.model.*; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.Tag; import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingClient; import com.amazonaws.services.elasticloadbalancing.model.*; import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersRequest; import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersResult; import com.amazonaws.services.elasticloadbalancing.model.DescribeTagsRequest; import com.amazonaws.services.elasticloadbalancing.model.DescribeTagsResult; import com.amazonaws.services.elasticloadbalancing.model.TagDescription; import com.amazonaws.services.route53.AmazonRoute53Client; import com.amazonaws.services.route53.model.*; import com.amazonaws.services.simpledb.AmazonSimpleDB; import com.amazonaws.services.simpledb.AmazonSimpleDBClient; import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.inject.Module; import com.netflix.simianarmy.CloudClient; import com.netflix.simianarmy.NotFoundException; import org.apache.commons.lang.Validate; import org.jclouds.ContextBuilder; import org.jclouds.aws.domain.SessionCredentials; import org.jclouds.compute.ComputeService; import org.jclouds.compute.ComputeServiceContext; import org.jclouds.compute.Utils; import org.jclouds.compute.domain.ComputeMetadata; import org.jclouds.compute.domain.NodeMetadata; import org.jclouds.compute.domain.NodeMetadataBuilder; import org.jclouds.domain.Credentials; import org.jclouds.domain.LoginCredentials; import org.jclouds.logging.slf4j.config.SLF4JLoggingModule; import org.jclouds.ssh.SshClient; import org.jclouds.ssh.jsch.config.JschSshClientModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** * The Class AWSClient. Simple Amazon EC2 and Amazon ASG client interface. */ public class AWSClient implements CloudClient { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(AWSClient.class); /** The region. */ private final String region; /** The plain name for AWS account */ private final String accountName; /** Maximum retry count for Simple DB */ private static final int SIMPLE_DB_MAX_RETRY = 11; private final AWSCredentialsProvider awsCredentialsProvider; private final ClientConfiguration awsClientConfig; private ComputeService jcloudsComputeService; /** * This constructor will let the AWS SDK obtain the credentials, which will * choose such in the following order: * *
    *
  • Environment Variables: {@code AWS_ACCESS_KEY_ID} and * {@code AWS_SECRET_KEY}
  • *
  • Java System Properties: {@code aws.accessKeyId} and * {@code aws.secretKey}
  • *
  • Instance Metadata Service, which provides the credentials associated * with the IAM role for the EC2 instance
  • *
* *

* If credentials are provided explicitly, use * {@link com.netflix.simianarmy.basic.BasicSimianArmyContext#exportCredentials(String, String)} * which will set them as System properties used by each AWS SDK call. *

* *

* Note: Avoid storing credentials received dynamically via the * {@link com.amazonaws.auth.InstanceProfileCredentialsProvider} as these will be rotated and * their renewal is handled by its * {@link com.amazonaws.auth.InstanceProfileCredentialsProvider#getCredentials()} method. *

* * @param region * the region * @see com.amazonaws.auth.DefaultAWSCredentialsProviderChain * @see com.amazonaws.auth.InstanceProfileCredentialsProvider * @see com.netflix.simianarmy.basic.BasicSimianArmyContext#exportCredentials(String, String) */ public AWSClient(String region) { this.region = region; this.accountName = "Default"; this.awsCredentialsProvider = null; this.awsClientConfig = null; } /** * The constructor allows you to provide your own AWS credentials provider. * @param region * the region * @param awsCredentialsProvider * the AWS credentials provider */ public AWSClient(String region, AWSCredentialsProvider awsCredentialsProvider) { this.region = region; this.accountName = "Default"; this.awsCredentialsProvider = awsCredentialsProvider; this.awsClientConfig = null; } /** * The constructor allows you to provide your own AWS client configuration. * @param region * the region * @param awsClientConfig * the AWS client configuration */ public AWSClient(String region, ClientConfiguration awsClientConfig) { this.region = region; this.accountName = "Default"; this.awsCredentialsProvider = null; this.awsClientConfig = awsClientConfig; } /** * The constructor allows you to provide your own AWS credentials provider and client config. * @param region * the region * @param awsCredentialsProvider * the AWS credentials provider * @param awsClientConfig * the AWS client configuration */ public AWSClient(String region, AWSCredentialsProvider awsCredentialsProvider, ClientConfiguration awsClientConfig) { this.region = region; this.accountName = "Default"; this.awsCredentialsProvider = awsCredentialsProvider; this.awsClientConfig = awsClientConfig; } /** * The Region. * * @return the region the client is configured to communicate with */ public String region() { return region; } /** * The accountName. * * @return the plain name for the aws account easier to identify which account * monkey is running in */ public String accountName() { return accountName; } /** * Amazon EC2 client. Abstracted to aid testing. * * @return the Amazon EC2 client */ protected AmazonEC2 ec2Client() { AmazonEC2 client; if (awsClientConfig == null) { if (awsCredentialsProvider == null) { client = new AmazonEC2Client(); } else { client = new AmazonEC2Client(awsCredentialsProvider); } } else { if (awsCredentialsProvider == null) { client = new AmazonEC2Client(awsClientConfig); } else { client = new AmazonEC2Client(awsCredentialsProvider, awsClientConfig); } } client.setEndpoint("ec2." + region + ".amazonaws.com"); return client; } /** * Amazon ASG client. Abstracted to aid testing. * * @return the Amazon Auto Scaling client */ protected AmazonAutoScalingClient asgClient() { AmazonAutoScalingClient client; if (awsClientConfig == null) { if (awsCredentialsProvider == null) { client = new AmazonAutoScalingClient(); } else { client = new AmazonAutoScalingClient(awsCredentialsProvider); } } else { if (awsCredentialsProvider == null) { client = new AmazonAutoScalingClient(awsClientConfig); } else { client = new AmazonAutoScalingClient(awsCredentialsProvider, awsClientConfig); } } client.setEndpoint("autoscaling." + region + ".amazonaws.com"); return client; } /** * Amazon ELB client. Abstracted to aid testing. * * @return the Amazon ELB client */ protected AmazonElasticLoadBalancingClient elbClient() { AmazonElasticLoadBalancingClient client; if (awsClientConfig == null) { if (awsCredentialsProvider == null) { client = new AmazonElasticLoadBalancingClient(); } else { client = new AmazonElasticLoadBalancingClient(awsCredentialsProvider); } } else { if (awsCredentialsProvider == null) { client = new AmazonElasticLoadBalancingClient(awsClientConfig); } else { client = new AmazonElasticLoadBalancingClient(awsCredentialsProvider, awsClientConfig); } } client.setEndpoint("elasticloadbalancing." + region + ".amazonaws.com"); return client; } /** * Amazon Route53 client. Abstracted to aid testing. * * @return the Amazon Route53 client */ protected AmazonRoute53Client route53Client() { AmazonRoute53Client client; if (awsClientConfig == null) { if (awsCredentialsProvider == null) { client = new AmazonRoute53Client(); } else { client = new AmazonRoute53Client(awsCredentialsProvider); } } else { if (awsCredentialsProvider == null) { client = new AmazonRoute53Client(awsClientConfig); } else { client = new AmazonRoute53Client(awsCredentialsProvider, awsClientConfig); } } client.setEndpoint("route53.amazonaws.com"); return client; } /** * Amazon SimpleDB client. * * @return the Amazon SimpleDB client */ public AmazonSimpleDB sdbClient() { AmazonSimpleDB client; ClientConfiguration cc = awsClientConfig; if (cc == null) { cc = new ClientConfiguration(); cc.setMaxErrorRetry(SIMPLE_DB_MAX_RETRY); } if (awsCredentialsProvider == null) { client = new AmazonSimpleDBClient(cc); } else { client = new AmazonSimpleDBClient(awsCredentialsProvider, cc); } // us-east-1 has special naming // http://docs.amazonwebservices.com/general/latest/gr/rande.html#sdb_region if (region == null || region.equals("us-east-1")) { client.setEndpoint("sdb.amazonaws.com"); } else { client.setEndpoint("sdb." + region + ".amazonaws.com"); } return client; } /** * Describe auto scaling groups. * * @return the list */ public List describeAutoScalingGroups() { return describeAutoScalingGroups((String[]) null); } /** * Describe a set of specific auto scaling groups. * * @param names the ASG names * @return the auto scaling groups */ public List describeAutoScalingGroups(String... names) { if (names == null || names.length == 0) { LOGGER.info(String.format("Getting all auto-scaling groups in region %s.", region)); } else { LOGGER.info(String.format("Getting auto-scaling groups for %d names in region %s.", names.length, region)); } List asgs = new LinkedList(); AmazonAutoScalingClient asgClient = asgClient(); DescribeAutoScalingGroupsRequest request = new DescribeAutoScalingGroupsRequest(); if (names != null) { request.setAutoScalingGroupNames(Arrays.asList(names)); } DescribeAutoScalingGroupsResult result = asgClient.describeAutoScalingGroups(request); asgs.addAll(result.getAutoScalingGroups()); while (result.getNextToken() != null) { request.setNextToken(result.getNextToken()); result = asgClient.describeAutoScalingGroups(request); asgs.addAll(result.getAutoScalingGroups()); } LOGGER.info(String.format("Got %d auto-scaling groups in region %s.", asgs.size(), region)); return asgs; } /** * Describe a set of specific ELBs. * * @param names the ELB names * @return the ELBs */ public List describeElasticLoadBalancers(String... names) { if (names == null || names.length == 0) { LOGGER.info(String.format("Getting all ELBs in region %s.", region)); } else { LOGGER.info(String.format("Getting ELBs for %d names in region %s.", names.length, region)); } AmazonElasticLoadBalancingClient elbClient = elbClient(); DescribeLoadBalancersRequest request = new DescribeLoadBalancersRequest().withLoadBalancerNames(names); DescribeLoadBalancersResult result = elbClient.describeLoadBalancers(request); List elbs = result.getLoadBalancerDescriptions(); LOGGER.info(String.format("Got %d ELBs in region %s.", elbs.size(), region)); return elbs; } /** * Describe a specific ELB. * * @param name the ELB names * @return the ELBs */ public LoadBalancerAttributes describeElasticLoadBalancerAttributes(String name) { LOGGER.info(String.format("Getting attributes for ELB with name '%s' in region %s.", name, region)); AmazonElasticLoadBalancingClient elbClient = elbClient(); DescribeLoadBalancerAttributesRequest request = new DescribeLoadBalancerAttributesRequest().withLoadBalancerName(name); DescribeLoadBalancerAttributesResult result = elbClient.describeLoadBalancerAttributes(request); LoadBalancerAttributes attrs = result.getLoadBalancerAttributes(); LOGGER.info(String.format("Got attributes for ELB with name '%s' in region %s.", name, region)); return attrs; } /** * Retreive the tags for a specific ELB. * * @param name the ELB names * @return the ELBs */ public List describeElasticLoadBalancerTags(String name) { LOGGER.info(String.format("Getting tags for ELB with name '%s' in region %s.", name, region)); AmazonElasticLoadBalancingClient elbClient = elbClient(); DescribeTagsRequest request = new DescribeTagsRequest().withLoadBalancerNames(name); DescribeTagsResult result = elbClient.describeTags(request); LOGGER.info(String.format("Got tags for ELB with name '%s' in region %s.", name, region)); return result.getTagDescriptions(); } /** * Describe a set of specific auto-scaling instances. * * @param instanceIds the instance ids * @return the instances */ public List describeAutoScalingInstances(String... instanceIds) { if (instanceIds == null || instanceIds.length == 0) { LOGGER.info(String.format("Getting all auto-scaling instances in region %s.", region)); } else { LOGGER.info(String.format("Getting auto-scaling instances for %d ids in region %s.", instanceIds.length, region)); } List instances = new LinkedList(); AmazonAutoScalingClient asgClient = asgClient(); DescribeAutoScalingInstancesRequest request = new DescribeAutoScalingInstancesRequest(); if (instanceIds != null) { request.setInstanceIds(Arrays.asList(instanceIds)); } DescribeAutoScalingInstancesResult result = asgClient.describeAutoScalingInstances(request); instances.addAll(result.getAutoScalingInstances()); while (result.getNextToken() != null) { request = request.withNextToken(result.getNextToken()); result = asgClient.describeAutoScalingInstances(request); instances.addAll(result.getAutoScalingInstances()); } LOGGER.info(String.format("Got %d auto-scaling instances.", instances.size())); return instances; } /** * Describe a set of specific instances. * * @param instanceIds the instance ids * @return the instances */ public List describeInstances(String... instanceIds) { if (instanceIds == null || instanceIds.length == 0) { LOGGER.info(String.format("Getting all EC2 instances in region %s.", region)); } else { LOGGER.info(String.format("Getting EC2 instances for %d ids in region %s.", instanceIds.length, region)); } List instances = new LinkedList(); AmazonEC2 ec2Client = ec2Client(); DescribeInstancesRequest request = new DescribeInstancesRequest(); if (instanceIds != null) { request.withInstanceIds(Arrays.asList(instanceIds)); } DescribeInstancesResult result = ec2Client.describeInstances(request); for (Reservation reservation : result.getReservations()) { instances.addAll(reservation.getInstances()); } LOGGER.info(String.format("Got %d EC2 instances in region %s.", instances.size(), region)); return instances; } /** * Describe a set of specific launch configurations. * * @param names the launch configuration names * @return the launch configurations */ public List describeLaunchConfigurations(String... names) { if (names == null || names.length == 0) { LOGGER.info(String.format("Getting all launch configurations in region %s.", region)); } else { LOGGER.info(String.format("Getting launch configurations for %d names in region %s.", names.length, region)); } List lcs = new LinkedList(); AmazonAutoScalingClient asgClient = asgClient(); DescribeLaunchConfigurationsRequest request = new DescribeLaunchConfigurationsRequest() .withLaunchConfigurationNames(names); DescribeLaunchConfigurationsResult result = asgClient.describeLaunchConfigurations(request); lcs.addAll(result.getLaunchConfigurations()); while (result.getNextToken() != null) { request.setNextToken(result.getNextToken()); result = asgClient.describeLaunchConfigurations(request); lcs.addAll(result.getLaunchConfigurations()); } LOGGER.info(String.format("Got %d launch configurations in region %s.", lcs.size(), region)); return lcs; } /** {@inheritDoc} */ @Override public void deleteAutoScalingGroup(String asgName) { Validate.notEmpty(asgName); LOGGER.info(String.format("Deleting auto-scaling group with name %s in region %s.", asgName, region)); AmazonAutoScalingClient asgClient = asgClient(); DeleteAutoScalingGroupRequest request = new DeleteAutoScalingGroupRequest() .withAutoScalingGroupName(asgName).withForceDelete(true); try { asgClient.deleteAutoScalingGroup(request); LOGGER.info(String.format("Deleted auto-scaling group with name %s in region %s.", asgName, region)); }catch(Exception e) { LOGGER.error("Got an exception deleting ASG " + asgName, e); } } /** {@inheritDoc} */ @Override public void deleteLaunchConfiguration(String launchConfigName) { Validate.notEmpty(launchConfigName); LOGGER.info(String.format("Deleting launch configuration with name %s in region %s.", launchConfigName, region)); AmazonAutoScalingClient asgClient = asgClient(); DeleteLaunchConfigurationRequest request = new DeleteLaunchConfigurationRequest() .withLaunchConfigurationName(launchConfigName); asgClient.deleteLaunchConfiguration(request); } /** {@inheritDoc} */ @Override public void deleteImage(String imageId) { Validate.notEmpty(imageId); LOGGER.info(String.format("Deleting image %s in region %s.", imageId, region)); AmazonEC2 ec2Client = ec2Client(); DeregisterImageRequest request = new DeregisterImageRequest(imageId); ec2Client.deregisterImage(request); } /** {@inheritDoc} */ @Override public void deleteVolume(String volumeId) { Validate.notEmpty(volumeId); LOGGER.info(String.format("Deleting volume %s in region %s.", volumeId, region)); AmazonEC2 ec2Client = ec2Client(); DeleteVolumeRequest request = new DeleteVolumeRequest().withVolumeId(volumeId); ec2Client.deleteVolume(request); } /** {@inheritDoc} */ @Override public void deleteSnapshot(String snapshotId) { Validate.notEmpty(snapshotId); LOGGER.info(String.format("Deleting snapshot %s in region %s.", snapshotId, region)); AmazonEC2 ec2Client = ec2Client(); DeleteSnapshotRequest request = new DeleteSnapshotRequest().withSnapshotId(snapshotId); ec2Client.deleteSnapshot(request); } /** {@inheritDoc} */ @Override public void deleteElasticLoadBalancer(String elbId) { Validate.notEmpty(elbId); LOGGER.info(String.format("Deleting ELB %s in region %s.", elbId, region)); AmazonElasticLoadBalancingClient elbClient = elbClient(); DeleteLoadBalancerRequest request = new DeleteLoadBalancerRequest(elbId); elbClient.deleteLoadBalancer(request); } /** {@inheritDoc} */ @Override public void deleteDNSRecord(String dnsName, String dnsType, String hostedZoneID) { Validate.notEmpty(dnsName); Validate.notEmpty(dnsType); if(dnsType.equals("A") || dnsType.equals("AAAA") || dnsType.equals("CNAME")) { LOGGER.info(String.format("Deleting DNS Route 53 record %s", dnsName)); AmazonRoute53Client route53Client = route53Client(); // AWS API requires us to query for the record first ListResourceRecordSetsRequest listRequest = new ListResourceRecordSetsRequest(hostedZoneID); listRequest.setMaxItems("1"); listRequest.setStartRecordType(dnsType); listRequest.setStartRecordName(dnsName); ListResourceRecordSetsResult listResult = route53Client.listResourceRecordSets(listRequest); if (listResult.getResourceRecordSets().size() < 1) { throw new NotFoundException("Could not find Route53 record for " + dnsName + " (" + dnsType + ") in zone " + hostedZoneID); } else { ResourceRecordSet resourceRecord = listResult.getResourceRecordSets().get(0); ArrayList changeList = new ArrayList<>(); Change recordChange = new Change(ChangeAction.DELETE, resourceRecord); changeList.add(recordChange); ChangeBatch recordChangeBatch = new ChangeBatch(changeList); ChangeResourceRecordSetsRequest request = new ChangeResourceRecordSetsRequest(hostedZoneID, recordChangeBatch); ChangeResourceRecordSetsResult result = route53Client.changeResourceRecordSets(request); } } else { LOGGER.error("dnsType must be one of 'A', 'AAAA', or 'CNAME'"); } } /** {@inheritDoc} */ @Override public void terminateInstance(String instanceId) { Validate.notEmpty(instanceId); LOGGER.info(String.format("Terminating instance %s in region %s.", instanceId, region)); try { ec2Client().terminateInstances(new TerminateInstancesRequest(Arrays.asList(instanceId))); } catch (AmazonServiceException e) { if (e.getErrorCode().equals("InvalidInstanceID.NotFound")) { throw new NotFoundException("AWS instance " + instanceId + " not found", e); } throw e; } } /** {@inheritDoc} */ public void setInstanceSecurityGroups(String instanceId, List groupIds) { Validate.notEmpty(instanceId); LOGGER.info(String.format("Removing all security groups from instance %s in region %s.", instanceId, region)); try { ModifyInstanceAttributeRequest request = new ModifyInstanceAttributeRequest(); request.setInstanceId(instanceId); request.setGroups(groupIds); ec2Client().modifyInstanceAttribute(request); } catch (AmazonServiceException e) { if (e.getErrorCode().equals("InvalidInstanceID.NotFound")) { throw new NotFoundException("AWS instance " + instanceId + " not found", e); } throw e; } } /** * Describe a set of specific EBS volumes. * * @param volumeIds the volume ids * @return the volumes */ public List describeVolumes(String... volumeIds) { if (volumeIds == null || volumeIds.length == 0) { LOGGER.info(String.format("Getting all EBS volumes in region %s.", region)); } else { LOGGER.info(String.format("Getting EBS volumes for %d ids in region %s.", volumeIds.length, region)); } AmazonEC2 ec2Client = ec2Client(); DescribeVolumesRequest request = new DescribeVolumesRequest(); if (volumeIds != null) { request.setVolumeIds(Arrays.asList(volumeIds)); } DescribeVolumesResult result = ec2Client.describeVolumes(request); List volumes = result.getVolumes(); LOGGER.info(String.format("Got %d EBS volumes in region %s.", volumes.size(), region)); return volumes; } /** * Describe a set of specific EBS snapshots. * * @param snapshotIds the snapshot ids * @return the snapshots */ public List describeSnapshots(String... snapshotIds) { if (snapshotIds == null || snapshotIds.length == 0) { LOGGER.info(String.format("Getting all EBS snapshots in region %s.", region)); } else { LOGGER.info(String.format("Getting EBS snapshotIds for %d ids in region %s.", snapshotIds.length, region)); } AmazonEC2 ec2Client = ec2Client(); DescribeSnapshotsRequest request = new DescribeSnapshotsRequest(); // Set the owner id to self to avoid getting snapshots from other accounts. request.withOwnerIds(Arrays.asList("self")); if (snapshotIds != null) { request.setSnapshotIds(Arrays.asList(snapshotIds)); } DescribeSnapshotsResult result = ec2Client.describeSnapshots(request); List snapshots = result.getSnapshots(); LOGGER.info(String.format("Got %d EBS snapshots in region %s.", snapshots.size(), region)); return snapshots; } @Override public void createTagsForResources(Map keyValueMap, String... resourceIds) { Validate.notNull(keyValueMap); Validate.notEmpty(keyValueMap); Validate.notNull(resourceIds); Validate.notEmpty(resourceIds); AmazonEC2 ec2Client = ec2Client(); List tags = new ArrayList(); for (Map.Entry entry : keyValueMap.entrySet()) { tags.add(new Tag(entry.getKey(), entry.getValue())); } CreateTagsRequest req = new CreateTagsRequest(Arrays.asList(resourceIds), tags); ec2Client.createTags(req); } /** * Describe a set of specific images. * * @param imageIds the image ids * @return the images */ public List describeImages(String... imageIds) { if (imageIds == null || imageIds.length == 0) { LOGGER.info(String.format("Getting all AMIs in region %s.", region)); } else { LOGGER.info(String.format("Getting AMIs for %d ids in region %s.", imageIds.length, region)); } AmazonEC2 ec2Client = ec2Client(); DescribeImagesRequest request = new DescribeImagesRequest(); if (imageIds != null) { request.setImageIds(Arrays.asList(imageIds)); } DescribeImagesResult result = ec2Client.describeImages(request); List images = result.getImages(); LOGGER.info(String.format("Got %d AMIs in region %s.", images.size(), region)); return images; } @Override public void detachVolume(String instanceId, String volumeId, boolean force) { Validate.notEmpty(instanceId); LOGGER.info(String.format("Detach volumes from instance %s in region %s.", instanceId, region)); try { DetachVolumeRequest detachVolumeRequest = new DetachVolumeRequest(); detachVolumeRequest.setForce(force); detachVolumeRequest.setInstanceId(instanceId); detachVolumeRequest.setVolumeId(volumeId); ec2Client().detachVolume(detachVolumeRequest); } catch (AmazonServiceException e) { if (e.getErrorCode().equals("InvalidInstanceID.NotFound")) { throw new NotFoundException("AWS instance " + instanceId + " not found", e); } throw e; } } @Override public List listAttachedVolumes(String instanceId, boolean includeRoot) { Validate.notEmpty(instanceId); LOGGER.info(String.format("Listing volumes attached to instance %s in region %s.", instanceId, region)); try { List volumeIds = new ArrayList(); for (Instance instance : describeInstances(instanceId)) { String rootDeviceName = instance.getRootDeviceName(); for (InstanceBlockDeviceMapping ibdm : instance.getBlockDeviceMappings()) { EbsInstanceBlockDevice ebs = ibdm.getEbs(); if (ebs == null) { continue; } String volumeId = ebs.getVolumeId(); if (Strings.isNullOrEmpty(volumeId)) { continue; } if (!includeRoot && rootDeviceName != null && rootDeviceName.equals(ibdm.getDeviceName())) { continue; } volumeIds.add(volumeId); } } return volumeIds; } catch (AmazonServiceException e) { if (e.getErrorCode().equals("InvalidInstanceID.NotFound")) { throw new NotFoundException("AWS instance " + instanceId + " not found", e); } throw e; } } /** * Describe a set of security groups. * * @param groupNames the names of the groups to find * @return a list of matching groups */ public List describeSecurityGroups(String... groupNames) { AmazonEC2 ec2Client = ec2Client(); DescribeSecurityGroupsRequest request = new DescribeSecurityGroupsRequest(); if (groupNames == null || groupNames.length == 0) { LOGGER.info(String.format("Getting all EC2 security groups in region %s.", region)); } else { LOGGER.info(String.format("Getting EC2 security groups for %d names in region %s.", groupNames.length, region)); request.withGroupNames(groupNames); } DescribeSecurityGroupsResult result; try { result = ec2Client.describeSecurityGroups(request); } catch (AmazonServiceException e) { if (e.getErrorCode().equals("InvalidGroup.NotFound")) { LOGGER.info("Got InvalidGroup.NotFound error for security groups; returning empty list"); return Collections.emptyList(); } throw e; } List securityGroups = result.getSecurityGroups(); LOGGER.info(String.format("Got %d EC2 security groups in region %s.", securityGroups.size(), region)); return securityGroups; } /** {@inheritDoc} */ public String createSecurityGroup(String instanceId, String name, String description) { String vpcId = getVpcId(instanceId); AmazonEC2 ec2Client = ec2Client(); CreateSecurityGroupRequest request = new CreateSecurityGroupRequest(); request.setGroupName(name); request.setDescription(description); request.setVpcId(vpcId); LOGGER.info(String.format("Creating EC2 security group %s.", name)); CreateSecurityGroupResult result = ec2Client.createSecurityGroup(request); return result.getGroupId(); } /** * Convenience wrapper around describeInstances, for a single instance id. * * @param instanceId id of instance to find * @return the instance info, or null if instance not found */ public Instance describeInstance(String instanceId) { Instance instance = null; for (Instance i : describeInstances(instanceId)) { if (instance != null) { throw new IllegalStateException("Duplicate instance: " + instanceId); } instance = i; } return instance; } /** {@inheritDoc} */ @Override public ComputeService getJcloudsComputeService() { if (jcloudsComputeService == null) { synchronized(this) { if (jcloudsComputeService == null) { AWSCredentials awsCredentials = awsCredentialsProvider.getCredentials(); String username = awsCredentials.getAWSAccessKeyId(); String password = awsCredentials.getAWSSecretKey(); Credentials credentials; if (awsCredentials instanceof AWSSessionCredentials) { AWSSessionCredentials awsSessionCredentials = (AWSSessionCredentials) awsCredentials; credentials = SessionCredentials.builder().accessKeyId(username).secretAccessKey(password) .sessionToken(awsSessionCredentials.getSessionToken()).build(); } else { credentials = new Credentials(username, password); } ComputeServiceContext jcloudsContext = ContextBuilder.newBuilder("aws-ec2") .credentialsSupplier(Suppliers.ofInstance(credentials)) .modules(ImmutableSet.of(new SLF4JLoggingModule(), new JschSshClientModule())) .buildView(ComputeServiceContext.class); this.jcloudsComputeService = jcloudsContext.getComputeService(); } } } return jcloudsComputeService; } /** {@inheritDoc} */ @Override public String getJcloudsId(String instanceId) { return this.region + "/" + instanceId; } @Override public SshClient connectSsh(String instanceId, LoginCredentials credentials) { ComputeService computeService = getJcloudsComputeService(); String jcloudsId = getJcloudsId(instanceId); NodeMetadata node = getJcloudsNode(computeService, jcloudsId); node = NodeMetadataBuilder.fromNodeMetadata(node).credentials(credentials).build(); Utils utils = computeService.getContext().utils(); SshClient ssh = utils.sshForNode().apply(node); ssh.connect(); return ssh; } private NodeMetadata getJcloudsNode(ComputeService computeService, String jcloudsId) { // Work around a jclouds bug / documentation issue... // TODO: Figure out what's broken, and eliminate this function // This should work (?): // Set nodes = computeService.listNodesByIds(Collections.singletonList(jcloudsId)); Set nodes = Sets.newHashSet(); for (ComputeMetadata n : computeService.listNodes()) { if (jcloudsId.equals(n.getId())) { nodes.add((NodeMetadata) n); } } if (nodes.isEmpty()) { LOGGER.warn("Unable to find jclouds node: {}", jcloudsId); for (ComputeMetadata n : computeService.listNodes()) { LOGGER.info("Did find node: {}", n); } throw new IllegalStateException("Unable to find node using jclouds: " + jcloudsId); } NodeMetadata node = Iterables.getOnlyElement(nodes); return node; } /** {@inheritDoc} */ @Override public String findSecurityGroup(String instanceId, String groupName) { String vpcId = getVpcId(instanceId); SecurityGroup found = null; List securityGroups = describeSecurityGroups(vpcId, groupName); for (SecurityGroup sg : securityGroups) { if (Objects.equal(vpcId, sg.getVpcId())) { if (found != null) { throw new IllegalStateException("Duplicate security groups found"); } found = sg; } } if (found == null) { return null; } return found.getGroupId(); } /** * Gets the VPC id for the given instance. * * @param instanceId * instance we're checking * @return vpc id, or null if not a vpc instance */ String getVpcId(String instanceId) { Instance awsInstance = describeInstance(instanceId); String vpcId = awsInstance.getVpcId(); if (Strings.isNullOrEmpty(vpcId)) { return null; } return vpcId; } /** {@inheritDoc} */ @Override public boolean canChangeInstanceSecurityGroups(String instanceId) { return null != getVpcId(instanceId); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/aws/chaos/ASGChaosCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.client.aws.chaos; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.Instance; import com.amazonaws.services.autoscaling.model.TagDescription; import com.netflix.simianarmy.GroupType; import com.netflix.simianarmy.basic.chaos.BasicChaosMonkey; import com.netflix.simianarmy.basic.chaos.BasicInstanceGroup; import com.netflix.simianarmy.chaos.ChaosCrawler; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.tunable.TunableInstanceGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The Class ASGChaosCrawler. This will crawl for all available AutoScalingGroups associated with the AWS account. */ public class ASGChaosCrawler implements ChaosCrawler { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ASGChaosCrawler.class); /** * The key of the tag that set the aggression coefficient */ private static final String CHAOS_MONKEY_AGGRESSION_COEFFICIENT_KEY = "chaosMonkey.aggressionCoefficient"; /** * The group types Types. */ public enum Types implements GroupType { /** only crawls AutoScalingGroups. */ ASG; } /** The aws client. */ private final AWSClient awsClient; /** * Instantiates a new basic chaos crawler. * * @param awsClient * the aws client */ public ASGChaosCrawler(AWSClient awsClient) { this.awsClient = awsClient; } /** {@inheritDoc} */ @Override public EnumSet groupTypes() { return EnumSet.allOf(Types.class); } /** {@inheritDoc} */ @Override public List groups() { return groups((String[]) null); } @Override public List groups(String... names) { List list = new LinkedList(); for (AutoScalingGroup asg : awsClient.describeAutoScalingGroups(names)) { InstanceGroup ig = getInstanceGroup(asg, findAggressionCoefficient(asg)); for (Instance inst : asg.getInstances()) { ig.addInstance(inst.getInstanceId()); } list.add(ig); } return list; } /** * Returns the desired InstanceGroup. If there is no set aggression coefficient, then it * returns the basic impl, otherwise it returns the tunable impl. * @param asg The autoscaling group * @return The appropriate {@link InstanceGroup} */ protected InstanceGroup getInstanceGroup(AutoScalingGroup asg, double aggressionCoefficient) { InstanceGroup instanceGroup; // if coefficient is 1 then the BasicInstanceGroup is fine, otherwise use Tunable if (aggressionCoefficient == 1.0) { instanceGroup = new BasicInstanceGroup(asg.getAutoScalingGroupName(), Types.ASG, awsClient.region(), asg.getTags()); } else { TunableInstanceGroup tunable = new TunableInstanceGroup(asg.getAutoScalingGroupName(), Types.ASG, awsClient.region(), asg.getTags()); tunable.setAggressionCoefficient(aggressionCoefficient); instanceGroup = tunable; } return instanceGroup; } /** * Reads tags on AutoScalingGroup looking for the tag for the aggression coefficient * and determines the coefficient value. The default value is 1 if there no tag or * if the value in the tag is not a parsable number. * * @param asg The AutoScalingGroup that might have an aggression coefficient tag * @return The set or default aggression coefficient. */ protected double findAggressionCoefficient(AutoScalingGroup asg) { List tagDescriptions = asg.getTags(); double aggression = 1.0; for (TagDescription tagDescription : tagDescriptions) { if ( CHAOS_MONKEY_AGGRESSION_COEFFICIENT_KEY.equalsIgnoreCase(tagDescription.getKey()) ) { String value = tagDescription.getValue(); // prevent NPE on parseDouble if (value == null) { break; } try { aggression = Double.parseDouble(value); LOGGER.info("Aggression coefficient of {} found for ASG {}", value, asg.getAutoScalingGroupName()); } catch (NumberFormatException e) { LOGGER.warn("Unparsable value of {} found in tag {} for ASG {}", value, CHAOS_MONKEY_AGGRESSION_COEFFICIENT_KEY, asg.getAutoScalingGroupName()); aggression = 1.0; } // stop looking break; } } return aggression; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/aws/chaos/FilteringChaosCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.client.aws.chaos; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.netflix.simianarmy.chaos.ChaosCrawler; import java.util.EnumSet; import java.util.List; /** * The Class FilteringChaosCrawler. This will filter the result from ASGChaosCrawler for all available AutoScalingGroups associated with the AWS account based on requested filter. */ public class FilteringChaosCrawler implements ChaosCrawler { private final ChaosCrawler crawler; private final Predicate predicate; public FilteringChaosCrawler(ChaosCrawler crawler, Predicate predicate) { this.crawler = crawler; this.predicate = predicate; } /** {@inheritDoc} */ @Override public EnumSet groupTypes() { return crawler.groupTypes(); } /** {@inheritDoc} */ @Override public List groups() { return filter(crawler.groups()); } /** {@inheritDoc} */ @Override public List groups(String... names) { return filter(crawler.groups(names)); } /** * Return the filtered list of InstanceGroups using the requested predicate. The filter is applied on the InstanceGroup retrieved from the ASGChaosCrawler class. * @param list list of InstanceGroups result of the chaos crawler * @return The appropriate {@link InstanceGroup} */ protected List filter(List list) { return Lists.newArrayList(Iterables.filter(list, predicate)); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/aws/chaos/TagPredicate.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.client.aws.chaos; import com.amazonaws.services.autoscaling.model.TagDescription; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.netflix.simianarmy.chaos.ChaosCrawler; /** * * The Class TagPredicate. This will apply the tag-key and the tag-value filter on the list of InstanceGroups . */ public class TagPredicate implements Predicate { private final String key, value; public TagPredicate(String key, String value) { this.key = key; this.value = value; } @Override public boolean apply(ChaosCrawler.InstanceGroup instanceGroup) { return Iterables.any(instanceGroup.tags(), new com.google.common.base.Predicate() { @Override public boolean apply(TagDescription tagDescription) { return tagDescription.getKey().equals(key) && tagDescription.getValue().equals(value); } }); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/edda/EddaClient.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.client.edda; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.client.MonkeyRestClient; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The REST client to access Edda to get the history of a cloud resource. */ public class EddaClient extends MonkeyRestClient { private static final Logger LOGGER = LoggerFactory.getLogger(EddaClient.class); private final MonkeyConfiguration config; /** * Constructor. * @param timeout the timeout in milliseconds * @param maxRetries the max number of retries * @param retryInterval the interval in milliseconds between retries * @param config the monkey configuration */ public EddaClient(int timeout, int maxRetries, int retryInterval, MonkeyConfiguration config) { super(timeout, maxRetries, retryInterval); this.config = config; } @Override public String getBaseUrl(String region) { Validate.notEmpty(region); String baseUrl = config.getStr("simianarmy.janitor.edda.endpoint." + region); if (StringUtils.isBlank(baseUrl)) { LOGGER.error(String.format("No endpoint of Edda is found for region %s.", region)); } return baseUrl; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/vsphere/PropertyBasedTerminationStrategy.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.simianarmy.client.vsphere; import java.rmi.RemoteException; import com.netflix.simianarmy.MonkeyConfiguration; import com.vmware.vim25.mo.VirtualMachine; /** * Terminates a VirtualMachine by setting the named property and resetting it. * * The following properties can be overridden in the client.properties * simianarmy.client.vsphere.terminationStrategy.property.name = PROPERTY_NAME * simianarmy.client.vsphere.terminationStrategy.property.value = PROPERTY_VALUE * * @author ingmar.krusch@immobilienscout24.de */ public class PropertyBasedTerminationStrategy implements TerminationStrategy { private final String propertyName; private final String propertyValue; /** * Reads property name simianarmy.client.vsphere.terminationStrategy.property.name * (default: Force Boot) and value simianarmy.client.vsphere.terminationStrategy.property.value * (default: server) from config. */ public PropertyBasedTerminationStrategy(MonkeyConfiguration config) { this.propertyName = config.getStrOrElse( "simianarmy.client.vsphere.terminationStrategy.property.name", "Force Boot"); this.propertyValue = config.getStrOrElse( "simianarmy.client.vsphere.terminationStrategy.property.value", "server"); } @Override public void terminate(VirtualMachine virtualMachine) throws RemoteException { virtualMachine.setCustomValue(getPropertyName(), getPropertyValue()); virtualMachine.resetVM_Task(); } public String getPropertyName() { return propertyName; } public String getPropertyValue() { return propertyValue; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/vsphere/TerminationStrategy.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.simianarmy.client.vsphere; import java.rmi.RemoteException; import com.vmware.vim25.mo.VirtualMachine; /** * Abstracts the concrete way a VirtualMachine is terminated. Implement this to fit to your infrastructure. * * @author ingmar.krusch@immobilienscout24.de */ public interface TerminationStrategy { /** * Terminate the given VirtualMachine. */ void terminate(VirtualMachine virtualMachine) throws RemoteException; } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/vsphere/VSphereClient.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.simianarmy.client.vsphere; import java.rmi.RemoteException; import java.util.List; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.netflix.simianarmy.client.aws.AWSClient; import com.vmware.vim25.mo.VirtualMachine; /** * This client describes the VSphere folders as AutoScalingGroup's containing the virtual machines that are directly in * that folder. The hierarchy is flattened this way. And it can terminate these VMs with the configured * TerminationStrategy. * * @author ingmar.krusch@immobilienscout24.de */ public class VSphereClient extends AWSClient { // private static final Logger LOGGER = LoggerFactory.getLogger(VSphereClient.class); private final TerminationStrategy terminationStrategy; private final VSphereServiceConnection connection; /** * Create the specific Client from the given strategy and connection. */ public VSphereClient(TerminationStrategy terminationStrategy, VSphereServiceConnection connection) { super("region-" + connection.getUrl()); this.terminationStrategy = terminationStrategy; this.connection = connection; } @Override public List describeAutoScalingGroups(String... names) { final VSphereGroups groups = new VSphereGroups(); try { connection.connect(); for (VirtualMachine virtualMachine : connection.describeVirtualMachines()) { String instanceId = virtualMachine.getName(); String groupName = virtualMachine.getParent().getName(); boolean shouldAddNamedGroup = true; if (names != null) { // TODO need to implement this feature!!! throw new RuntimeException("This feature (selecting groups by name) is not implemented yet"); } if (shouldAddNamedGroup) { groups.addInstance(instanceId, groupName); } } } finally { connection.disconnect(); } return groups.asList(); } @Override /** * reinstall the given instance. If it is powered down this will be ignored and the * reinstall occurs the next time the machine is powered up. */ public void terminateInstance(String instanceId) { try { connection.connect(); VirtualMachine virtualMachine = connection.getVirtualMachineById(instanceId); this.terminationStrategy.terminate(virtualMachine); } catch (RemoteException e) { throw new AmazonServiceException("cannot destroy & recreate " + instanceId, e); } finally { connection.disconnect(); } } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/vsphere/VSphereContext.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.simianarmy.client.vsphere; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.basic.BasicChaosMonkeyContext; /** * This Context extends the BasicContext in order to provide a different client: the VSphereClient. * * @author ingmar.krusch@immobilienscout24.de */ public class VSphereContext extends BasicChaosMonkeyContext { @Override protected void createClient() { MonkeyConfiguration config = configuration(); final PropertyBasedTerminationStrategy terminationStrategy = new PropertyBasedTerminationStrategy(config); final VSphereServiceConnection connection = new VSphereServiceConnection(config); final VSphereClient client = new VSphereClient(terminationStrategy, connection); setCloudClient(client); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/vsphere/VSphereGroups.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.simianarmy.client.vsphere; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.Instance; /** * Wraps the creation and grouping of Instance's in AutoScalingGroup's. * * @author ingmar.krusch@immobilienscout24.de */ class VSphereGroups { private final Map map = new HashMap(); /** * Get all AutoScalingGroup's that have been added. */ public List asList() { ArrayList list = new ArrayList(map.values()); Collections.sort(list, new Comparator() { @Override public int compare(AutoScalingGroup o1, AutoScalingGroup o2) { return o1.getAutoScalingGroupName().compareTo(o2.getAutoScalingGroupName()); } }); return list; } /** * Add the given instance to the named group. */ public void addInstance(final String instanceId, final String groupName) { if (!map.containsKey(groupName)) { final AutoScalingGroup asg = new AutoScalingGroup(); asg.setAutoScalingGroupName(groupName); map.put(groupName, asg); } final AutoScalingGroup asg = map.get(groupName); Instance instance = new Instance(); instance.setInstanceId(instanceId); asg.getInstances().add(instance); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/client/vsphere/VSphereServiceConnection.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.simianarmy.client.vsphere; import java.net.MalformedURLException; import java.net.URL; import java.rmi.RemoteException; import java.util.Arrays; import com.amazonaws.AmazonServiceException; import com.netflix.simianarmy.MonkeyConfiguration; import com.vmware.vim25.InvalidProperty; import com.vmware.vim25.RuntimeFault; import com.vmware.vim25.mo.InventoryNavigator; import com.vmware.vim25.mo.ManagedEntity; import com.vmware.vim25.mo.ServiceInstance; import com.vmware.vim25.mo.VirtualMachine; /** * Wraps the connection to VSphere and handles the raw service calls. * * The following properties can be overridden in the client.properties * simianarmy.client.vsphere.url = https://YOUR_VSPHERE_SERVER/sdk * simianarmy.client.vsphere.username = YOUR_SERVICE_ACCOUNT_USERNAME * simianarmy.client.vsphere.password = YOUR_SERVICE_ACCOUNT_PASSWORD * * @author ingmar.krusch@immobilienscout24.de */ public class VSphereServiceConnection { /** The type of managedEntity we operate on are virtual machines. */ public static final String VIRTUAL_MACHINE_TYPE_NAME = "VirtualMachine"; /** The username that is used to connect to VSpehere Center. */ private String username = null; /** The password that is used to connect to VSpehere Center. */ private String password = null; /** The url that is used to connect to VSpehere Center. */ private String url = null; /** The ServiceInstance that is used to issue multiple requests to VSpehere Center. */ private ServiceInstance service = null; /** * Constructor. */ public VSphereServiceConnection(MonkeyConfiguration config) { this.url = config.getStr("simianarmy.client.vsphere.url"); this.username = config.getStr("simianarmy.client.vsphere.username"); this.password = config.getStr("simianarmy.client.vsphere.password"); } /** disconnect from the service if not already disconnected. */ public void disconnect() { if (service != null) { service.getServerConnection().logout(); service = null; } } /** connect to the service if not already connected. */ public void connect() throws AmazonServiceException { try { if (service == null) { service = new ServiceInstance(new URL(url), username, password, true); } } catch (RemoteException e) { throw new AmazonServiceException("cannot connect to VSphere", e); } catch (MalformedURLException e) { throw new AmazonServiceException("cannot connect to VSphere", e); } } /** * Gets the named VirtualMachine. */ public VirtualMachine getVirtualMachineById(String instanceId) throws RemoteException { InventoryNavigator inventoryNavigator = getInventoryNavigator(); VirtualMachine virtualMachine = (VirtualMachine) inventoryNavigator.searchManagedEntity( VIRTUAL_MACHINE_TYPE_NAME, instanceId); return virtualMachine; } /** * Return all VirtualMachines from VSpehere Center. * * @throws AmazonServiceException * If there is any communication error or if no VirtualMachine's are found. */ public VirtualMachine[] describeVirtualMachines() throws AmazonServiceException { ManagedEntity[] mes = null; try { mes = getInventoryNavigator().searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME); } catch (InvalidProperty e) { throw new AmazonServiceException("cannot query VSphere", e); } catch (RuntimeFault e) { throw new AmazonServiceException("cannot query VSphere", e); } catch (RemoteException e) { throw new AmazonServiceException("cannot query VSphere", e); } if (mes == null || mes.length == 0) { throw new AmazonServiceException( "vsphere returned zero entities of type \"" + VIRTUAL_MACHINE_TYPE_NAME + "\"" ); } else { return Arrays.copyOf(mes, mes.length, VirtualMachine[].class); } } protected InventoryNavigator getInventoryNavigator() { return new InventoryNavigator(service.getRootFolder()); } public String getUsername() { return username; } public String getPassword() { return password; } public String getUrl() { return url; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/AutoScalingGroup.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import com.google.common.collect.Lists; import org.apache.commons.lang.Validate; import java.util.Collection; import java.util.Collections; /** * The class implementing the auto scaling groups. */ public class AutoScalingGroup { private final String name; private final Collection instances = Lists.newArrayList(); private boolean isSuspended; /** * Constructor. * @param name * the name of the auto scaling group * @param instances * the instance ids in the auto scaling group */ public AutoScalingGroup(String name, String... instances) { Validate.notNull(instances); this.name = name; for (String instance : instances) { this.instances.add(instance); } this.isSuspended = false; } /** * Gets the name of the auto scaling group. * @return * the name of the auto scaling group */ public String getName() { return name; } /** * * Gets the instances of the auto scaling group. * @return * the instances of the auto scaling group */ public Collection getInstances() { return Collections.unmodifiableCollection(instances); } /** * Gets the flag to indicate whether the ASG is suspended. * @return true if the ASG is suspended, false otherwise */ public boolean isSuspended() { return isSuspended; } /** * Sets the flag to indicate whether the ASG is suspended. * @param suspended true if the ASG is suspended, false otherwise */ public void setSuspended(boolean suspended) { isSuspended = suspended; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/Cluster.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; /** * The class implementing clusters. Cluster is the basic unit of conformity check. It can be a single ASG or * a group of ASGs that belong to the same application, for example, a cluster in the Asgard deployment system. */ public class Cluster { public static final String OWNER_EMAIL = "ownerEmail"; public static final String CLUSTER = "cluster"; public static final String REGION = "region"; public static final String IS_CONFORMING = "isConforming"; public static final String IS_OPTEDOUT = "isOptedOut"; public static final String UPDATE_TIMESTAMP = "updateTimestamp"; public static final String EXCLUDED_RULES = "excludedRules"; public static final String CONFORMITY_RULES = "conformityRules"; public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss"); private final String name; private final Collection autoScalingGroups = Lists.newArrayList(); private final String region; private String ownerEmail; private Date updateTime; private final Map conformities = Maps.newHashMap(); private final Collection excludedConformityRules = Sets.newHashSet(); private boolean isConforming; private boolean isOptOutOfConformity; private final Set soloInstances = Sets.newHashSet(); /** * Constructor. * @param name * the name of the cluster * @param autoScalingGroups * the auto scaling groups in the cluster */ public Cluster(String name, String region, AutoScalingGroup... autoScalingGroups) { Validate.notNull(name); Validate.notNull(region); Validate.notNull(autoScalingGroups); this.name = name; this.region = region; for (AutoScalingGroup asg : autoScalingGroups) { this.autoScalingGroups.add(asg); } } /** * Constructor. * @param name * the name of the cluster * @param soloInstances * the list of all instances */ public Cluster(String name, String region, Set soloInstances) { Validate.notNull(name); Validate.notNull(region); Validate.notNull(soloInstances); this.name = name; this.region = region; for (String soleInstance : soloInstances) { this.soloInstances.add(soleInstance); } } /** * Gets the name of the cluster. * @return * the name of the cluster */ public String getName() { return name; } /** * Gets the region of the cluster. * @return * the region of the cluster */ public String getRegion() { return region; } /** * * Gets the auto scaling groups of the auto scaling group. * @return * the auto scaling groups in the cluster */ public Collection getAutoScalingGroups() { return Collections.unmodifiableCollection(autoScalingGroups); } /** * Gets the owner email of the cluster. * @return * the owner email of the cluster */ public String getOwnerEmail() { return ownerEmail; } /** * Sets the owner email of the cluster. * @param ownerEmail * the owner email of the cluster */ public void setOwnerEmail(String ownerEmail) { this.ownerEmail = ownerEmail; } /** * Gets the update time of the cluster. * @return * the update time of the cluster */ public Date getUpdateTime() { return new Date(updateTime.getTime()); } /** * Sets the update time of the cluster. * @param updateTime * the update time of the cluster */ public void setUpdateTime(Date updateTime) { this.updateTime = new Date(updateTime.getTime()); } /** * Gets all conformity check information of the cluster. * @return * all conformity check information of the cluster */ public Collection getConformties() { return conformities.values(); } /** * Gets the conformity information for a conformity rule. * @param rule * the conformity rule * @return * the conformity for the rule */ public Conformity getConformity(ConformityRule rule) { Validate.notNull(rule); return conformities.get(rule.getName()); } /** * Updates the cluster with a new conformity check result. * @param conformity * the conformity to update * @return * the cluster itself * */ public Cluster updateConformity(Conformity conformity) { Validate.notNull(conformity); conformities.put(conformity.getRuleId(), conformity); return this; } /** * Clears the conformity check results. */ public void clearConformities() { conformities.clear(); } /** * Gets the boolean flag to indicate whether the cluster is conforming to * all non-excluded conformity rules. * @return * true if the cluster is conforming against all non-excluded rules, * false otherwise */ public boolean isConforming() { return isConforming; } /** * Sets the boolean flag to indicate whether the cluster is conforming to * all non-excluded conformity rules. * @param conforming * true if the cluster is conforming against all non-excluded rules, * false otherwise */ public void setConforming(boolean conforming) { isConforming = conforming; } /** * Gets names of all excluded conformity rules for this cluster. * @return * names of all excluded conformity rules for this cluster */ public Collection getExcludedRules() { return Collections.unmodifiableCollection(excludedConformityRules); } /** * Excludes rules for the cluster. * @param ruleIds * the rule ids to exclude * @return * the cluster itself */ public Cluster excludeRules(String... ruleIds) { Validate.notNull(ruleIds); for (String ruleId : ruleIds) { Validate.notNull(ruleId); excludedConformityRules.add(ruleId.trim()); } return this; } /** * Gets the flag to indicate whether the cluster is opted out of Conformity monkey. * @return true if the cluster is not handled by Conformity monkey, false otherwise */ public boolean isOptOutOfConformity() { return isOptOutOfConformity; } /** * Sets the flag to indicate whether the cluster is opted out of Conformity monkey. * @param optOutOfConformity * true if the cluster is not handled by Conformity monkey, false otherwise */ public void setOptOutOfConformity(boolean optOutOfConformity) { isOptOutOfConformity = optOutOfConformity; } /** * Gets a map from fields of resources to corresponding values. Values are represented * as Strings so they can be displayed or stored in databases like SimpleDB. * @return a map from field name to field value */ public Map getFieldToValueMap() { Map map = Maps.newHashMap(); putToMapIfNotNull(map, CLUSTER, name); putToMapIfNotNull(map, REGION, region); putToMapIfNotNull(map, OWNER_EMAIL, ownerEmail); putToMapIfNotNull(map, UPDATE_TIMESTAMP, String.valueOf(DATE_FORMATTER.print(updateTime.getTime()))); putToMapIfNotNull(map, IS_CONFORMING, String.valueOf(isConforming)); putToMapIfNotNull(map, IS_OPTEDOUT, String.valueOf(isOptOutOfConformity)); putToMapIfNotNull(map, EXCLUDED_RULES, StringUtils.join(excludedConformityRules, ",")); List ruleIds = Lists.newArrayList(); for (Conformity conformity : conformities.values()) { map.put(conformity.getRuleId(), StringUtils.join(conformity.getFailedComponents(), ",")); ruleIds.add(conformity.getRuleId()); } putToMapIfNotNull(map, CONFORMITY_RULES, StringUtils.join(ruleIds, ",")); return map; } /** * Parse a map from field name to value to a cluster. * @param fieldToValue the map from field name to value * @return the cluster that is de-serialized from the map */ public static Cluster parseFieldToValueMap(Map fieldToValue) { Validate.notNull(fieldToValue); Cluster cluster = new Cluster(fieldToValue.get(CLUSTER), fieldToValue.get(REGION)); cluster.setOwnerEmail(fieldToValue.get(OWNER_EMAIL)); cluster.setConforming(Boolean.parseBoolean(fieldToValue.get(IS_CONFORMING))); cluster.setOptOutOfConformity(Boolean.parseBoolean(fieldToValue.get(IS_OPTEDOUT))); cluster.excludeRules(StringUtils.split(fieldToValue.get(EXCLUDED_RULES), ",")); cluster.setUpdateTime(new Date(DATE_FORMATTER.parseDateTime(fieldToValue.get(UPDATE_TIMESTAMP)).getMillis())); for (String ruleId : StringUtils.split(fieldToValue.get(CONFORMITY_RULES), ",")) { cluster.updateConformity(new Conformity(ruleId, Lists.newArrayList(StringUtils.split(fieldToValue.get(ruleId), ",")))); } return cluster; } private static void putToMapIfNotNull(Map map, String key, String value) { Validate.notNull(map); Validate.notNull(key); if (value != null) { map.put(key, value); } } public Set getSoloInstances() { return Collections.unmodifiableSet(soloInstances); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/ClusterCrawler.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import java.util.List; /** * The interface of the crawler for Conformity Monkey to get the cluster information. */ public interface ClusterCrawler { /** * Gets the up to date information for a collection of clusters. When the input argument is null * or empty, the method returns all clusters. * * @param clusterNames * the cluster names * @return the list of clusters */ List clusters(String... clusterNames); /** * Gets the owner email for a cluster to set the ownerEmail field when crawl. * @param cluster * the cluster * @return the owner email of the cluster */ String getOwnerEmailForCluster(Cluster cluster); /** * Updates the excluded conformity rules for the given cluster. * @param cluster */ void updateExcludedConformityRules(Cluster cluster); } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/Conformity.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import com.google.common.collect.Lists; import org.apache.commons.lang.Validate; import java.util.Collection; import java.util.Collections; /** * The class defining the result of a conformity check. */ public class Conformity { private final String ruleId; private final Collection failedComponents = Lists.newArrayList(); /** * Constructor. * @param ruleId * the conformity rule id * @param failedComponents * the components that cause the conformity check to fail, if there is * no failed components, it means the conformity check passes. */ public Conformity(String ruleId, Collection failedComponents) { Validate.notNull(ruleId); Validate.notNull(failedComponents); this.ruleId = ruleId; for (String failedComponent : failedComponents) { this.failedComponents.add(failedComponent); } } /** * Gets the conformity rule id. * @return * the conformity rule id */ public String getRuleId() { return ruleId; } /** * Gets the components that cause the conformity check to fail. * @return * the components that cause the conformity check to fail */ public Collection getFailedComponents() { return Collections.unmodifiableCollection(failedComponents); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/ConformityClusterTracker.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import java.util.List; /** * The interface that defines the tracker to manage clusters for Conformity monkey to use. */ public interface ConformityClusterTracker { /** * Adds a cluster to the tracker. If the cluster with the same name already exists, * the method updates the record with the cluster parameter. * @param cluster * the cluster to add or update */ void addOrUpdate(Cluster cluster); /** * Gets the list of clusters in a list of regions. * @param regions * the regions of the clusters, when the parameter is null or empty, the method returns * clusters from all regions * @return list of clusters in the given regions */ List getAllClusters(String... regions); /** * Gets the list of non-conforming clusters in a list of regions. * @param regions the regions of the clusters, when the parameter is null or empty, the method returns * clusters from all regions * @return list of clusters in the given regions */ List getNonconformingClusters(String... regions); /** * Gets the cluster with a specific name from . * @param name the cluster name * @param region the region of the cluster * @return the cluster with the name */ Cluster getCluster(String name, String region); /** * Deletes a list of clusters from the tracker. * @param clusters the list of clusters to delete. The parameter cannot be null. If it is empty, * no cluster is deleted. */ void deleteClusters(Cluster... clusters); } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/ConformityEmailBuilder.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import com.netflix.simianarmy.AbstractEmailBuilder; import java.util.Collection; import java.util.Map; /** The abstract class for building Conformity monkey email notifications. */ public abstract class ConformityEmailBuilder extends AbstractEmailBuilder { /** * Sets the map from an owner email to the clusters that belong to the owner * and need to send notifications for. * @param emailToClusters the map from owner email to the owned clusters * @param rules all conformity rules that are used to find the description of each rule to display */ public abstract void setEmailToClusters(Map> emailToClusters, Collection rules); } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/ConformityEmailNotifier.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.aws.AWSEmailNotifier; import org.apache.commons.lang.Validate; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.List; import java.util.Map; /** * The email notifier implemented for Janitor Monkey. */ public class ConformityEmailNotifier extends AWSEmailNotifier { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ConformityEmailNotifier.class); private static final String UNKNOWN_EMAIL = "UNKNOWN"; private final Collection regions = Lists.newArrayList(); private final String defaultEmail; private final List ccEmails = Lists.newArrayList(); private final ConformityClusterTracker clusterTracker; private final ConformityEmailBuilder emailBuilder; private final String sourceEmail; private final Map> invalidEmailToClusters = Maps.newHashMap(); private final Collection rules = Lists.newArrayList(); private final int openHour; private final int closeHour; /** * The Interface Context. */ public interface Context { /** * Gets the Amazon Simple Email Service client. * @return the Amazon Simple Email Service client */ AmazonSimpleEmailServiceClient sesClient(); /** * Gets the open hour the email notifications are sent. * @return * the open hour the email notifications are sent */ int openHour(); /** * Gets the close hour the email notifications are sent. * @return * the close hour the email notifications are sent */ int closeHour(); /** * Gets the source email the notifier uses to send email. * @return the source email */ String sourceEmail(); /** * Gets the default email the notifier sends to when there is no owner specified for a cluster. * @return the default email */ String defaultEmail(); /** * Gets the regions the notifier is running in. * @return the regions the notifier is running in. */ Collection regions(); /** Gets the Conformity Monkey's cluster tracker. * @return the Conformity Monkey's cluster tracker */ ConformityClusterTracker clusterTracker(); /** Gets the Conformity email builder. * @return the Conformity email builder */ ConformityEmailBuilder emailBuilder(); /** Gets the cc email addresses. * @return the cc email addresses */ String[] ccEmails(); /** * Gets all the conformity rules. * @return all conformity rules. */ Collection rules(); } /** * Constructor. * @param ctx the context. */ public ConformityEmailNotifier(Context ctx) { super(ctx.sesClient()); this.openHour = ctx.openHour(); this.closeHour = ctx.closeHour(); for (String region : ctx.regions()) { this.regions.add(region); } this.defaultEmail = ctx.defaultEmail(); this.clusterTracker = ctx.clusterTracker(); this.emailBuilder = ctx.emailBuilder(); String[] ctxCCs = ctx.ccEmails(); if (ctxCCs != null) { for (String ccEmail : ctxCCs) { this.ccEmails.add(ccEmail); } } this.sourceEmail = ctx.sourceEmail(); Validate.notNull(ctx.rules()); for (ConformityRule rule : ctx.rules()) { rules.add(rule); } } /** * Gets all the clusters that are not conforming and sends email notifications to the owners. */ public void sendNotifications() { int currentHour = DateTime.now().getHourOfDay(); if (currentHour < openHour || currentHour > closeHour) { LOGGER.info("It is not the time for Conformity Monkey to send notifications. You can change " + "simianarmy.conformity.notification.openHour and simianarmy.conformity.notification.openHour" + " to make it work at this hour."); return; } validateEmails(); Map> emailToClusters = Maps.newHashMap(); for (Cluster cluster : clusterTracker.getNonconformingClusters(regions.toArray(new String[regions.size()]))) { if (cluster.isOptOutOfConformity()) { LOGGER.info(String.format("Cluster %s is opted out of Conformity Monkey so no notification is sent.", cluster.getName())); continue; } if (!cluster.isConforming()) { String email = cluster.getOwnerEmail(); if (!isValidEmail(email)) { if (defaultEmail != null) { LOGGER.info(String.format("Email %s is not valid, send to the default email address %s", email, defaultEmail)); putEmailAndCluster(emailToClusters, defaultEmail, cluster); } else { if (email == null) { email = UNKNOWN_EMAIL; } LOGGER.info(String.format("Email %s is not valid and default email is not set for cluster %s", email, cluster.getName())); putEmailAndCluster(invalidEmailToClusters, email, cluster); } } else { putEmailAndCluster(emailToClusters, email, cluster); } } else { LOGGER.debug(String.format("Cluster %s is conforming so no notification needs to be sent.", cluster.getName())); } } emailBuilder.setEmailToClusters(emailToClusters, rules); for (Map.Entry> entry : emailToClusters.entrySet()) { String email = entry.getKey(); String emailBody = emailBuilder.buildEmailBody(email); String subject = buildEmailSubject(email); sendEmail(email, subject, emailBody); for (Cluster cluster : entry.getValue()) { LOGGER.debug(String.format("Notification is sent for cluster %s to %s", cluster.getName(), email)); } LOGGER.info(String.format("Email notification has been sent to %s for %d clusters.", email, entry.getValue().size())); } } @Override public String buildEmailSubject(String to) { return String.format("Conformity Monkey Notification for %s", to); } @Override public String[] getCcAddresses(String to) { return ccEmails.toArray(new String[ccEmails.size()]); } @Override public String getSourceAddress(String to) { return sourceEmail; } private void validateEmails() { if (defaultEmail != null) { Validate.isTrue(isValidEmail(defaultEmail), String.format("Default email %s is invalid", defaultEmail)); } if (ccEmails != null) { for (String ccEmail : ccEmails) { Validate.isTrue(isValidEmail(ccEmail), String.format("CC email %s is invalid", ccEmail)); } } } private void putEmailAndCluster(Map> map, String email, Cluster cluster) { Collection clusters = map.get(email); if (clusters == null) { clusters = Lists.newArrayList(); map.put(email, clusters); } clusters.add(cluster); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/ConformityMonkey.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.MonkeyType; import java.util.Collection; /** * The abstract class for Conformity Monkey. */ public abstract class ConformityMonkey extends Monkey { /** * The Interface Context. */ public interface Context extends Monkey.Context { /** * Configuration. * * @return the monkey configuration */ MonkeyConfiguration configuration(); /** * Crawler that gets information of all clusters for conformity check. * @return all clusters for conformity check */ ClusterCrawler clusterCrawler(); /** * Conformity rule engine. * @return the Conformity rule engine */ ConformityRuleEngine ruleEngine(); /** * Email notifier used to send notifications by the Conformity monkey. * @return the email notifier */ ConformityEmailNotifier emailNotifier(); /** * The regions the monkey is running in. * @return the regions the monkey is running in. */ Collection regions(); /** * The tracker of the clusters for conformity monkey to check. * @return the tracker of the clusters for conformity monkey to check. */ ConformityClusterTracker clusterTracker(); /** * Gets the flag to indicate whether the monkey is leashed. * @return true if the monkey is leashed and does not make real change or send notifications to * cluster owners, false otherwise. */ boolean isLeashed(); } /** The context. */ private final Context ctx; /** * Instantiates a new Conformity monkey. * * @param ctx * the context. */ public ConformityMonkey(Context ctx) { super(ctx); this.ctx = ctx; } /** * The monkey Type. */ public enum Type implements MonkeyType { /** Conformity monkey. */ CONFORMITY } /** {@inheritDoc} */ @Override public final Type type() { return Type.CONFORMITY; } /** {@inheritDoc} */ @Override public Context context() { return ctx; } /** {@inheritDoc} */ @Override public abstract void doMonkeyBusiness(); } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/ConformityRule.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; /** * Interface for a conformity check rule. */ public interface ConformityRule { /** * Performs the conformity check against the rule. * @param cluster * the cluster to check for conformity * @return * the conformity check result */ Conformity check(Cluster cluster); /** * Gets the name/id of the rule. * @return * the name of the rule */ String getName(); /** * Gets the human-readable reason to explain why the cluster is not conforming. * @return the human-readable reason to explain why the cluster is not conforming */ String getNonconformingReason(); } ================================================ FILE: src/main/java/com/netflix/simianarmy/conformity/ConformityRuleEngine.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.conformity; import com.google.common.collect.Lists; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; /** * The class implementing the conformity rule engine. */ public class ConformityRuleEngine { private static final Logger LOGGER = LoggerFactory.getLogger(ConformityRuleEngine.class); private final Collection rules = Lists.newArrayList(); /** * Checks whether a cluster is conforming or not against the rules in the engine. This * method runs the checks the cluster against all the rules. * * @param cluster * the cluster * @return true if the cluster is conforming, false otherwise. */ public boolean check(Cluster cluster) { Validate.notNull(cluster); cluster.clearConformities(); for (ConformityRule rule : rules) { if (!cluster.getExcludedRules().contains(rule.getName())) { LOGGER.info(String.format("Running conformity rule %s on cluster %s", rule.getName(), cluster.getName())); cluster.updateConformity(rule.check(cluster)); } else { LOGGER.info(String.format("Conformity rule %s is excluded on cluster %s", rule.getName(), cluster.getName())); } } boolean isConforming = true; for (Conformity conformity : cluster.getConformties()) { if (!conformity.getFailedComponents().isEmpty()) { isConforming = false; } } cluster.setConforming(isConforming); return isConforming; } /** * Add a conformity rule. * * @param rule * The conformity rule to add. * @return The Conformity rule engine object. */ public ConformityRuleEngine addRule(ConformityRule rule) { Validate.notNull(rule); rules.add(rule); return this; } /** * Gets all conformity rules in the rule engine. * @return all conformity rules in the rule engine */ public Collection rules() { return Collections.unmodifiableCollection(rules); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/AbstractJanitor.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import java.util.*; import com.google.common.collect.Maps; import com.netflix.servo.annotations.DataSourceType; import com.netflix.servo.annotations.Monitor; import com.netflix.servo.annotations.MonitorTags; import com.netflix.servo.monitor.BasicCounter; import com.netflix.servo.monitor.Counter; import com.netflix.servo.monitor.MonitorConfig; import com.netflix.servo.monitor.Monitors; import com.netflix.servo.tag.BasicTagList; import com.netflix.servo.tag.TagList; import com.netflix.simianarmy.*; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.Resource.CleanupState; import com.netflix.simianarmy.janitor.JanitorMonkey.EventTypes; import com.netflix.simianarmy.janitor.JanitorMonkey.Type; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An abstract implementation of Janitor. It marks resources that the rule engine considers * invalid as cleanup candidate and sets the expected termination date. It also removes the * cleanup candidate flag from resources that no longer exist or the rule engine no longer * considers invalid due to change of conditions. For resources marked as cleanup candidates * and the expected termination date is passed, the janitor removes the resources from the * cloud. */ public abstract class AbstractJanitor implements Janitor, DryRunnableJanitor { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJanitor.class); /** Tags to attach to servo metrics */ @MonitorTags protected TagList tags; private final String region; /** The region the janitor is running in. */ public String getRegion() { return region; } /** * The rule engine used to decide if a resource should be a cleanup * candidate. */ private final JanitorRuleEngine ruleEngine; /** The janitor crawler to get resources from the cloud. */ private final JanitorCrawler crawler; /** The resource type that the janitor is responsible for to clean up. **/ private final ResourceType resourceType; /** The janitor resource tracker that is responsible for keeping track of * resource status. */ private final JanitorResourceTracker resourceTracker; private final Collection markedResources = new ArrayList(); private final Collection cleanedResources = new ArrayList(); private final Collection unmarkedResources = new ArrayList(); private final Collection failedToCleanResources = new ArrayList(); private final Collection skippedVanishedOrValidResources = new ArrayList<>(); private final MonkeyCalendar calendar; private final MonkeyConfiguration config; /** Flag to indicate whether the Janitor is leashed. */ private boolean leashed; private final MonkeyRecorder recorder; /** The number of resources that have been checked on this run. */ private int checkedResourcesCount; private Counter cleanupDryRunFailureCount = new BasicCounter(MonitorConfig.builder("dryRunCleanupFailures").build()); /** * Sets the flag to indicate if the janitor is leashed. * * @param isLeashed true if the the janitor is leased, false otherwise. */ protected void setLeashed(boolean isLeashed) { this.leashed = isLeashed; } /** * The Interface Context. */ public interface Context { /** Region. * * @return the region */ String region(); /** * Configuration. * * @return the monkey configuration */ MonkeyConfiguration configuration(); /** * Calendar. * * @return the monkey calendar */ MonkeyCalendar calendar(); /** * Janitor rule engine. * @return the janitor rule engine */ JanitorRuleEngine janitorRuleEngine(); /** * Janitor crawler. * * @return the chaos crawler */ JanitorCrawler janitorCrawler(); /** * Janitor resource tracker. * * @return the janitor resource tracker */ JanitorResourceTracker janitorResourceTracker(); /** * Recorder. * * @return the recorder to record events */ MonkeyRecorder recorder(); } /** * Constructor. * @param ctx the context * @param resourceType the resource type the janitor is taking care */ public AbstractJanitor(Context ctx, ResourceType resourceType) { Validate.notNull(ctx); Validate.notNull(resourceType); this.region = ctx.region(); Validate.notNull(region); this.ruleEngine = ctx.janitorRuleEngine(); Validate.notNull(ruleEngine); this.crawler = ctx.janitorCrawler(); Validate.notNull(crawler); this.resourceTracker = ctx.janitorResourceTracker(); Validate.notNull(resourceTracker); this.calendar = ctx.calendar(); Validate.notNull(calendar); this.config = ctx.configuration(); Validate.notNull(config); // By default the janitor is leashed. this.leashed = config.getBoolOrElse("simianarmy.janitor.leashed", true); this.resourceType = resourceType; Validate.notNull(resourceType); // recorder could be null and no events are recorded when it is. this.recorder = ctx.recorder(); // setup servo tags, currently just tag each published metric with the region this.tags = BasicTagList.of("simianarmy.janitor.region", ctx.region()); // register this janitor with servo String monitorObjName = String.format("simianarmy.janitor.%s.%s", this.resourceType.name(), this.region); Monitors.registerObject(monitorObjName, this); } @Override public ResourceType getResourceType() { return resourceType; } /** * Clears this object's internal resource lists in preparation for a new * run. * * This is an optional method as regular Janitor processing will * automatically clear resource lists as it runs. * * This method offers an explicit clear so that the resources will be * consistent across the run. For example, when starting a run after a * previous run has finished, cleanedResources will be holding the cleaned * resources from the prior run until cleanupResources() is called. By * calling prepareToRun() first, the resource lists will be consistent * for the entire run. */ public void prepareToRun() { markedResources.clear(); unmarkedResources.clear(); checkedResourcesCount = 0; cleanedResources.clear(); failedToCleanResources.clear(); } /** * Marks all resources obtained from the crawler as cleanup candidate if * the janitor rule engine thinks so. */ @Override public void markResources() { if (config.getBoolOrElse("simianarmy.janitor.skipMark", false)) { LOGGER.info("*****SKIPPING MARKING {}****", resourceType); return ; } markedResources.clear(); unmarkedResources.clear(); checkedResourcesCount = 0; Map trackedMarkedResources = getTrackedMarkedResources(); List crawledResources = crawler.resources(resourceType); LOGGER.info("Looking for cleanup candidate in {} crawled resources. LeashMode={}", crawledResources.size(), leashed); Date now = calendar.now().getTime(); for (Resource resource : crawledResources) { checkedResourcesCount++; Resource trackedResource = trackedMarkedResources.get(resource.getId()); if (!ruleEngine.isValid(resource)) { // If the resource is already marked, ignore it if (trackedResource != null) { LOGGER.debug("Resource {} is already marked. LeashMode={}", resource.getId(), leashed); continue; } LOGGER.info("Marking resource {} of type {} with expected termination time as {} LeashMode={}" , resource.getId(), resource.getResourceType(), resource.getExpectedTerminationTime(), leashed); resource.setState(CleanupState.MARKED); resource.setMarkTime(now); resourceTracker.addOrUpdate(resource); if (!leashed && recorder != null) { Event evt = recorder.newEvent(Type.JANITOR, EventTypes.MARK_RESOURCE, resource, resource.getId()); recorder.recordEvent(evt); } postMark(resource); markedResources.add(resource); } else if (trackedResource != null) { // The resource was marked and now the rule engine does not consider it as a cleanup candidate. // So the janitor needs to unmark the resource. LOGGER.info("Unmarking resource {} LeashMode={}", resource.getId(), leashed); resource.setState(CleanupState.UNMARKED); resourceTracker.addOrUpdate(resource); if (!leashed && recorder != null) { Event evt = recorder.newEvent( Type.JANITOR, EventTypes.UNMARK_RESOURCE, resource, resource.getId()); recorder.recordEvent(evt); } unmarkedResources.add(resource); } } // Unmark the resources that are terminated by user so not returned by the crawler. unmarkUserTerminatedResources(crawledResources, trackedMarkedResources); } /** * Gets the existing resources that are marked as cleanup candidate. Allowing the subclass to override for e.g. * to handle multi-region. * @return the map from resource id to marked resource */ protected Map getTrackedMarkedResources() { Map trackedMarkedResources = Maps.newHashMap(); for (Resource resource : resourceTracker.getResources(resourceType, Resource.CleanupState.MARKED, region)) { trackedMarkedResources.put(resource.getId(), resource); } return trackedMarkedResources; } /** * Cleans up all cleanup candidates that are OK to remove. */ @Override public void cleanupResources() { cleanedResources.clear(); failedToCleanResources.clear(); skippedVanishedOrValidResources.clear(); Map trackedMarkedResources = getTrackedMarkedResources(); LOGGER.info("Checking {} marked resources for cleanup. LeashMode={}", trackedMarkedResources.size(), leashed); Date now = calendar.now().getTime(); for (Resource markedResource : trackedMarkedResources.values()) { if (config.getBoolOrElse("simianarmy.janitor.skipVanishedOrValidResources", false)) { // find matching crawled resource. This ensures we always have the freshest resource. List matchingCrawledResources = Optional.ofNullable(crawler.resources(markedResource.getId())) .orElse(Collections.emptyList()); LOGGER.info("Rechecking resource {} before deletion {} - matching candidates {}", markedResource, markedResource.getResourceType(), matchingCrawledResources); Optional crawledResource = matchingCrawledResources.stream() .filter(r -> r.equals(markedResource)) .findFirst(); if (!crawledResource.isPresent() || ruleEngine.isValid(crawledResource.get())) { skippedVanishedOrValidResources.add(markedResource); LOGGER.warn("Skipping resource {} that either no longer exists or is now valid", markedResource); continue; } } if (canClean(markedResource, now)) { LOGGER.info("Cleaning up resource {} of type {}. LeashMode={}", markedResource.getId(), markedResource.getResourceType().name(), leashed); try { if (leashed) { cleanupDryRun(markedResource.cloneResource()); } else { cleanup(markedResource); markedResource.setActualTerminationTime(now); markedResource.setState(Resource.CleanupState.JANITOR_TERMINATED); resourceTracker.addOrUpdate(markedResource); if (recorder != null) { Event evt = recorder.newEvent(Type.JANITOR, EventTypes.CLEANUP_RESOURCE, markedResource, markedResource.getId()); recorder.recordEvent(evt); } postCleanup(markedResource); cleanedResources.add(markedResource); } } catch (Exception e) { String message; if (e instanceof DryRunnableJanitorException) { message = String.format("Failed Dry Run cleanup of resource %s of type %s. LeashMode=%b", markedResource.getId(), markedResource.getResourceType().name(), leashed); cleanupDryRunFailureCount.increment(); } else { message = String.format("Failed to clean up the resource %s of type %s. LeashMode=%b", markedResource.getId(), markedResource.getResourceType().name(), leashed); failedToCleanResources.add(markedResource); } LOGGER.error(message, e); } } } } /** Determines if the input resource can be cleaned. The Janitor calls this method * before cleaning up a resource and only cleans the resource when the method returns * true. A resource is considered to be OK to clean if * 1) it is marked as cleanup candidates * 2) the expected termination time is already passed * 3) the owner has already been notified about the cleanup * 4) the resource is not opted out of Janitor monkey * The method can be overridden in subclasses. * @param resource the resource the Janitor considers to clean * @param now the time that represents the current time * @return true if the resource is OK to clean, false otherwise */ protected boolean canClean(Resource resource, Date now) { return resource.getState() == Resource.CleanupState.MARKED && !resource.isOptOutOfJanitor() && resource.getExpectedTerminationTime() != null && resource.getExpectedTerminationTime().before(now) && resource.getNotificationTime() != null && resource.getNotificationTime().before(now); } /** * Implements required operations after a resource is marked. * @param resource The resource that is marked */ protected abstract void postMark(Resource resource); /** * Cleans a resource up, e.g. deleting the resource from the cloud. * @param resource The resource that is cleaned up. */ protected abstract void cleanup(Resource resource); /** * Implements required operations after a resource is cleaned. * @param resource The resource that is cleaned up. */ protected abstract void postCleanup(Resource resource); /** gets the resources marked in the last run of the Janitor. */ public Collection getMarkedResources() { return Collections.unmodifiableCollection(markedResources); } /** gets the resources unmarked in the last run of the Janitor. */ public Collection getUnmarkedResources() { return Collections.unmodifiableCollection(unmarkedResources); } /** gets the resources cleaned in the last run of the Janitor. */ public Collection getCleanedResources() { return Collections.unmodifiableCollection(cleanedResources); } /** gets the resources that failed to be cleaned in the last run of the Janitor. */ public Collection getFailedToCleanResources() { return Collections.unmodifiableCollection(failedToCleanResources); } private void unmarkUserTerminatedResources(List crawledResources, Map trackedMarkedResources) { Set crawledResourceIds = new HashSet(); for (Resource crawledResource : crawledResources) { crawledResourceIds.add(crawledResource.getId()); } if (config.getBoolOrElse("simianarmy.janitor.unmarkResourceNotReturnedByCrawler", false)) { for (Resource markedResource : trackedMarkedResources.values()) { if (!crawledResourceIds.contains(markedResource.getId())) { // The resource does not exist anymore. LOGGER.info("Resource {} is not returned by the crawler. It should already be terminated. LeashMode={}", markedResource.getId(), leashed); markedResource.setState(Resource.CleanupState.USER_TERMINATED); resourceTracker.addOrUpdate(markedResource); unmarkedResources.add(markedResource); } } } } @Monitor(name="cleanedResourcesCount", type=DataSourceType.GAUGE) public int getResourcesCleanedCount() { return cleanedResources.size(); } @Monitor(name="markedResourcesCount", type=DataSourceType.GAUGE) public int getMarkedResourcesCount() { return markedResources.size(); } @Monitor(name="failedToCleanResourcesCount", type=DataSourceType.GAUGE) public int getFailedToCleanResourcesCount() { return failedToCleanResources.size(); } @Monitor(name="unmarkedResourcesCount", type=DataSourceType.GAUGE) public int getUnmarkedResourcesCount() { return unmarkedResources.size(); } @Monitor(name="checkedResourcesCount", type=DataSourceType.GAUGE) public int getCheckedResourcesCount() { return checkedResourcesCount; } @Monitor(name="skippedVanishedOrValidResources", type = DataSourceType.GAUGE) public int skippedVanishedOrValidResources() { return skippedVanishedOrValidResources.size(); } public Counter getCleanupDryRunFailureCount() { return cleanupDryRunFailureCount; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/DryRunnableJanitor.java ================================================ /* * * Copyright 2017 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import com.netflix.simianarmy.Resource; public interface DryRunnableJanitor extends Janitor { default void cleanupDryRun(Resource markedResource) throws DryRunnableJanitorException { // NO-OP } } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/DryRunnableJanitorException.java ================================================ /* * * Copyright 2017 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; public class DryRunnableJanitorException extends Exception { public DryRunnableJanitorException(String message) { super(message); } public DryRunnableJanitorException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/Janitor.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import com.netflix.simianarmy.ResourceType; /** * The interface for a janitor that performs the mark and cleanup operations for * cloud resources of a resource type. */ public interface Janitor { /** * Gets the resource type the janitor is cleaning up. * @return the resource type the janitor is cleaning up. */ ResourceType getResourceType(); /** * Mark cloud resources as cleanup candidates and remove the marks for resources * that no longer exist or should not be cleanup candidates anymore. */ void markResources(); /** * Clean the resources up that are marked as cleanup candidates when appropriate. */ void cleanupResources(); } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/JanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import java.util.EnumSet; import java.util.List; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; /** * The crawler for janitor monkey. */ public interface JanitorCrawler { /** * Resource types. * * @return the type of resources this crawler crawls */ EnumSet resourceTypes(); /** * Resources crawled by this crawler for a specific resource type. * * @param resourceType the resource type * @return the list */ List resources(ResourceType resourceType); /** * Gets the up to date information for a collection of resource ids. When the input argument is null * or empty, the method returns all resources. * * @param resourceIds * the resource ids * @return the list of resources */ List resources(String... resourceIds); /** * Gets the owner email for a resource to set the ownerEmail field when crawl. * @param resource the resource * @return the owner email of the resource */ String getOwnerEmailForResource(Resource resource); } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/JanitorEmailBuilder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import java.util.Collection; import java.util.Map; import com.netflix.simianarmy.AbstractEmailBuilder; import com.netflix.simianarmy.Resource; /** The abstract class for building Janitor monkey email notifications. */ public abstract class JanitorEmailBuilder extends AbstractEmailBuilder { /** * Sets the map from an owner email to the resources that belong to the owner * and need to send notifications for. * @param emailToResources the map from owner email to the owned resource */ public abstract void setEmailToResources(Map> emailToResources); } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/JanitorEmailNotifier.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.Resource.CleanupState; import com.netflix.simianarmy.aws.AWSEmailNotifier; import org.apache.commons.lang.Validate; import org.apache.commons.lang.StringUtils; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** The email notifier implemented for Janitor Monkey. */ public class JanitorEmailNotifier extends AWSEmailNotifier { /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(JanitorEmailNotifier.class); private static final String UNKNOWN_EMAIL = "UNKNOWN"; /** * If the scheduled termination date is within 2 hours of notification date + headsup days, * we don't need to extend the termination date. */ private static final int HOURS_IN_MARGIN = 2; private final String region; private final String defaultEmail; private final List ccEmails; private final JanitorResourceTracker resourceTracker; private final JanitorEmailBuilder emailBuilder; private final MonkeyCalendar calendar; private final int daysBeforeTermination; private final String sourceEmail; private final String ownerEmailDomain; private final Map> invalidEmailToResources = new HashMap>(); /** * The Interface Context. */ public interface Context { /** * Gets the Amazon Simple Email Service client. * @return the Amazon Simple Email Service client */ AmazonSimpleEmailServiceClient sesClient(); /** * Gets the source email the notifier uses to send email. * @return the source email */ String sourceEmail(); /** * Gets the default email the notifier sends to when there is no owner specified for a resource. * @return the default email */ String defaultEmail(); /** * Gets the number of days a notification is sent before the expected termination date.. * @return the number of days a notification is sent before the expected termination date. */ int daysBeforeTermination(); /** * Gets the region the notifier is running in. * @return the region the notifier is running in. */ String region(); /** Gets the janitor resource tracker. * @return the janitor resource tracker */ JanitorResourceTracker resourceTracker(); /** Gets the janitor email builder. * @return the janitor email builder */ JanitorEmailBuilder emailBuilder(); /** Gets the calendar. * @return the calendar */ MonkeyCalendar calendar(); /** Gets the cc email addresses. * @return the cc email addresses */ String[] ccEmails(); /** Get the default domain of email addresses. * @return the default domain of email addresses */ String ownerEmailDomain(); } /** * Constructor. * @param ctx the context. */ public JanitorEmailNotifier(Context ctx) { super(ctx.sesClient()); this.region = ctx.region(); this.defaultEmail = ctx.defaultEmail(); this.daysBeforeTermination = ctx.daysBeforeTermination(); this.resourceTracker = ctx.resourceTracker(); this.emailBuilder = ctx.emailBuilder(); this.calendar = ctx.calendar(); this.ccEmails = new ArrayList(); String[] ctxCCs = ctx.ccEmails(); if (ctxCCs != null) { for (String ccEmail : ctxCCs) { this.ccEmails.add(ccEmail); } } this.sourceEmail = ctx.sourceEmail(); this.ownerEmailDomain = ctx.ownerEmailDomain(); } /** * Gets all the resources that are marked and no notifications have been sent. Send email notifications * for these resources. If there is a valid email address in the ownerEmail field of the resource, send * to that address. Otherwise send to the default email address. */ public void sendNotifications() { validateEmails(); Map> emailToResources = new HashMap>(); invalidEmailToResources.clear(); for (Resource r : getMarkedResources()) { if (r.isOptOutOfJanitor()) { LOGGER.info(String.format("Resource %s is opted out of Janitor Monkey so no notification is sent.", r.getId())); continue; } if (canNotify(r)) { String email = r.getOwnerEmail(); if (email != null && !email.contains("@") && StringUtils.isNotBlank(this.ownerEmailDomain)) { email = String.format("%s@%s", email, this.ownerEmailDomain); } if (!isValidEmail(email)) { if (defaultEmail != null) { LOGGER.info(String.format("Email %s is not valid, send to the default email address %s", email, defaultEmail)); putEmailAndResource(emailToResources, defaultEmail, r); } else { if (email == null) { email = UNKNOWN_EMAIL; } LOGGER.info(String.format("Email %s is not valid and default email is not set for resource %s", email, r.getId())); putEmailAndResource(invalidEmailToResources, email, r); } } else { putEmailAndResource(emailToResources, email, r); } } else { LOGGER.debug(String.format("Not the time to send notification for resource %s", r.getId())); } } emailBuilder.setEmailToResources(emailToResources); Date now = calendar.now().getTime(); for (Map.Entry> entry : emailToResources.entrySet()) { String email = entry.getKey(); String emailBody = emailBuilder.buildEmailBody(email); String subject = buildEmailSubject(email); sendEmail(email, subject, emailBody); for (Resource r : entry.getValue()) { LOGGER.debug(String.format("Notification is sent for resource %s", r.getId())); r.setNotificationTime(now); resourceTracker.addOrUpdate(r); } LOGGER.info(String.format("Email notification has been sent to %s for %d resources.", email, entry.getValue().size())); } } /** * Gets the marked resources for notification. Allow overriding in subclasses. * @return the marked resources */ protected Collection getMarkedResources() { return resourceTracker.getResources(null, CleanupState.MARKED, region); } private void validateEmails() { if (defaultEmail != null) { Validate.isTrue(isValidEmail(defaultEmail), String.format("Default email %s is invalid", defaultEmail)); } if (ccEmails != null) { for (String ccEmail : ccEmails) { Validate.isTrue(isValidEmail(ccEmail), String.format("CC email %s is invalid", ccEmail)); } } } @Override public String buildEmailSubject(String email) { return String.format("Janitor Monkey Notification for %s", email); } /** * Decides if it is time for sending notification for the resource. This method can be * overridden in subclasses so notifications can be send earlier or later. * @param resource the resource * @return true if it is OK to send notification now, otherwise false. */ protected boolean canNotify(Resource resource) { Validate.notNull(resource); if (resource.getState() != CleanupState.MARKED || resource.isOptOutOfJanitor()) { return false; } Date notificationTime = resource.getNotificationTime(); // We don't want to send notification too early (since things may change) or too late (we need // to give owners enough time to take actions. Date windowStart = new Date(new DateTime( calendar.getBusinessDay(calendar.now().getTime(), daysBeforeTermination).getTime()) .minusHours(HOURS_IN_MARGIN).getMillis()); Date windowEnd = calendar.getBusinessDay(calendar.now().getTime(), daysBeforeTermination + 1); Date terminationDate = resource.getExpectedTerminationTime(); if (notificationTime == null || notificationTime.getTime() == 0 || resource.getMarkTime().after(notificationTime)) { // remarked after a notification if (!terminationDate.before(windowStart) && !terminationDate.after(windowEnd)) { // The expected termination time is close enough for sending notification return true; } else if (terminationDate.before(windowStart)) { // The expected termination date is too close. To give the owner time to take possible actions, // we extend the expected termination time here. LOGGER.info(String.format("It is less than %d days before the expected termination date," + " of resource %s, extending the termination time to %s.", daysBeforeTermination, resource.getId(), windowStart)); resource.setExpectedTerminationTime(windowStart); resourceTracker.addOrUpdate(resource); return true; } else { return false; } } return false; } /** * Gets the map from invalid email address to the resources that were supposed to be sent to the address. * * @return the map from invalid address to resources that failed to be delivered */ public Map> getInvalidEmailToResources() { return Collections.unmodifiableMap(invalidEmailToResources); } @Override public String[] getCcAddresses(String to) { return ccEmails.toArray(new String[ccEmails.size()]); } @Override public String getSourceAddress(String to) { return sourceEmail; } private void putEmailAndResource( Map> map, String email, Resource resource) { Collection resources = map.get(email); if (resources == null) { resources = new ArrayList(); map.put(email, resources); } resources.add(resource); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/JanitorMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.MonkeyType; import java.util.List; /** * The abstract class for a Janitor Monkey. */ public abstract class JanitorMonkey extends Monkey { /** The key name of the Janitor tag used to tag resources. */ public static final String JANITOR_TAG = "janitor"; /** The key name of the Janitor meta tag used to tag resources. */ public static final String JANITOR_META_TAG = "JANITOR_META"; /** The key name of the tag instance used to tag resources. */ public static final String INSTANCE_TAG_KEY = "instance"; /** The key name of the tag detach time used to tag resources. */ public static final String DETACH_TIME_TAG_KEY = "detachTime"; /** * The Interface Context. */ public interface Context extends Monkey.Context { /** * Configuration. * * @return the monkey configuration */ MonkeyConfiguration configuration(); /** * Janitors run by this monkey. * @return the janitors */ List janitors(); /** * Email notifier used to send notifications by the janitor monkey. * @return the email notifier */ JanitorEmailNotifier emailNotifier(); /** * The region the monkey is running in. * @return the region the monkey is running in. */ String region(); /** * The accountName the monkey is running in. * @return the accountName the monkey is running in. */ String accountName(); /** * The Janitor resource tracker. * @return the Janitor resource tracker. */ JanitorResourceTracker resourceTracker(); } /** The context. */ private final Context ctx; /** * Instantiates a new janitor monkey. * * @param ctx * the context. */ public JanitorMonkey(Context ctx) { super(ctx); this.ctx = ctx; } /** * The monkey Type. */ public static enum Type implements MonkeyType { /** janitor monkey. */ JANITOR } /** * The event types that this monkey causes. */ public enum EventTypes implements EventType { /** Marking a resource as a cleanup candidate. */ MARK_RESOURCE, /** Un-Marking a resource. */ UNMARK_RESOURCE, /** Clean up a resource. */ CLEANUP_RESOURCE, /** Opt in a resource. */ OPT_IN_RESOURCE, /** Opt out a resource. */ OPT_OUT_RESOURCE } /** {@inheritDoc} */ @Override public final Type type() { return Type.JANITOR; } /** {@inheritDoc} */ @Override public Context context() { return ctx; } /** {@inheritDoc} */ @Override public abstract void doMonkeyBusiness(); /** * Opt in a resource for Janitor Monkey. * @param resourceId the resource id * @return the opt-in event */ public abstract Event optInResource(String resourceId); /** * Opt out a resource for Janitor Monkey. * @param resourceId the resource id * @return the opt-out event */ public abstract Event optOutResource(String resourceId); /** * Opt in a resource for Janitor Monkey. * @param resourceId the resource id * @param region the region of the resource * @return the opt-in event */ public abstract Event optInResource(String resourceId, String region); /** * Opt out a resource for Janitor Monkey. * @param resourceId the resource id * @param region the region of the resource * @return the opt-out event */ public abstract Event optOutResource(String resourceId, String region); } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/JanitorResourceTracker.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.ResourceType; import java.util.List; /** * The interface to track the resources marked/cleaned by the Janitor Monkey. * */ public interface JanitorResourceTracker { /** * Adds a resource to the tracker. If the resource with the same id already exists * in the tracker, the method updates the record with the resource parameter. * @param resource the resource to add or update */ void addOrUpdate(Resource resource); /** Gets the list of resources of a specific resource type and cleanup state in a region. * * @param resourceType the resource type * @param state the cleanup state of the resources * @param region the region of the resources, when the parameter is null, the method returns * resources from all regions * @return list of resources that match the resource type, state and region */ List getResources(ResourceType resourceType, Resource.CleanupState state, String region); /** Gets the resource of a specific id. * * @param resourceId the resource id * @return the resource that matches the resource id */ Resource getResource(String resourceId); /** Gets the resource of a specific id. * * @param resourceId the resource id * @param regionId the region id * @return the resource that matches the resource id and region */ Resource getResource(String resourceId, String regionId); } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/JanitorRuleEngine.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import com.netflix.simianarmy.Resource; import java.util.List; /** * The interface for janitor rule engine that can decide if a resource should be a candidate of cleanup * based on a collection of rules. */ public interface JanitorRuleEngine { /** * Decides whether the resource should be a candidate of cleanup based on the underlying rules. * * @param resource * The resource * @return true if the resource is valid and should not be a candidate of cleanup based on the underlying rules, * false otherwise. */ boolean isValid(Resource resource); /** * Add a rule to decide if a resource should be a candidate for cleanup. * * @param rule * The rule to decide if a resource should be a candidate for cleanup. * @return The JanitorRuleEngine object. */ JanitorRuleEngine addRule(Rule rule); /** * Add a rule to decide if a resource should be excluded for cleanup. * Exclusion rules are evaluated before regular rules. If a resource * matches an exclusion rule, it is excluded from all other cleanup rules. * * @param rule * The rule to decide if a resource should be excluded for cleanup. * @return The JanitorRuleEngine object. */ JanitorRuleEngine addExclusionRule(Rule rule); /** * Get rules to find out what's planned for enforcement. * * @return An ArrayList of Rules. */ List getRules(); /** * Get rules to find out what's excluded for enforcement. * * @return An ArrayList of Rules. */ List getExclusionRules(); } ================================================ FILE: src/main/java/com/netflix/simianarmy/janitor/Rule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import com.netflix.simianarmy.Resource; /** * The rule implementing a logic to decide if a resource should be considered as a candidate of cleanup. */ public interface Rule { /** * Decides whether the resource should be a candidate of cleanup based on the underlying rule. When * the rule considers the resource as a candidate of cleanup, it sets the expected termination time * and termination reason of the resource. * * @param resource * The resource * @return true if the resource is valid and is not for cleanup, false otherwise */ boolean isValid(Resource resource); } ================================================ FILE: src/main/java/com/netflix/simianarmy/resources/chaos/ChaosMonkeyResource.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.resources.chaos; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import com.google.common.base.Strings; import com.netflix.simianarmy.Monkey; import com.sun.jersey.spi.resource.Singleton; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.MappingJsonFactory; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.simianarmy.FeatureNotEnabledException; import com.netflix.simianarmy.InstanceGroupNotFoundException; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.MonkeyRunner; import com.netflix.simianarmy.NotFoundException; import com.netflix.simianarmy.chaos.ChaosMonkey; import com.netflix.simianarmy.chaos.ChaosType; import com.netflix.simianarmy.chaos.ShutdownInstanceChaosType; /** * The Class ChaosMonkeyResource for json REST apis. */ @Path("/v1/chaos") @Produces(MediaType.APPLICATION_JSON) @Singleton public class ChaosMonkeyResource { /** The Constant JSON_FACTORY. */ private static final MappingJsonFactory JSON_FACTORY = new MappingJsonFactory(); /** The monkey. */ private ChaosMonkey monkey = null; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(ChaosMonkeyResource.class); /** * Instantiates a chaos monkey resource with a specific chaos monkey. * * @param monkey * the chaos monkey */ public ChaosMonkeyResource(ChaosMonkey monkey) { this.monkey = monkey; } /** * Instantiates a chaos monkey resource using a registered chaos monkey from factory. */ public ChaosMonkeyResource() { for (Monkey runningMonkey : MonkeyRunner.getInstance().getMonkeys()) { if (runningMonkey instanceof ChaosMonkey) { this.monkey = (ChaosMonkey) runningMonkey; break; } } if (this.monkey == null) { LOGGER.info("Creating a new Chaos monkey instance for the resource."); this.monkey = MonkeyRunner.getInstance().factory(ChaosMonkey.class); } } /** * Gets the chaos events. Creates GET /api/v1/chaos api which outputs the chaos events in json. Users can specify * cgi query params to filter the results and use "since" query param to set the start of a timerange. "since" should * be specified in milliseconds since the epoch. * * @param uriInfo * the uri info * @return the chaos events json response * @throws IOException * Signals that an I/O exception has occurred. */ @GET public Response getChaosEvents(@Context UriInfo uriInfo) throws IOException { Map query = new HashMap(); Date date = null; for (Map.Entry> pair : uriInfo.getQueryParameters().entrySet()) { if (pair.getValue().isEmpty()) { continue; } if (pair.getKey().equals("since")) { date = new Date(Long.parseLong(pair.getValue().get(0))); } else { query.put(pair.getKey(), pair.getValue().get(0)); } } // if "since" not set, default to 24 hours ago if (date == null) { Calendar now = monkey.context().calendar().now(); now.add(Calendar.DAY_OF_YEAR, -1); date = now.getTime(); } List evts = monkey.context().recorder() .findEvents(ChaosMonkey.Type.CHAOS, ChaosMonkey.EventTypes.CHAOS_TERMINATION, query, date); ByteArrayOutputStream baos = new ByteArrayOutputStream(); JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos, JsonEncoding.UTF8); gen.writeStartArray(); for (Event evt : evts) { gen.writeStartObject(); gen.writeStringField("monkeyType", evt.monkeyType().name()); gen.writeStringField("eventId", evt.id()); gen.writeStringField("eventType", evt.eventType().name()); gen.writeNumberField("eventTime", evt.eventTime().getTime()); gen.writeStringField("region", evt.region()); for (Map.Entry pair : evt.fields().entrySet()) { gen.writeStringField(pair.getKey(), pair.getValue()); } gen.writeEndObject(); } gen.writeEndArray(); gen.close(); return Response.status(Response.Status.OK).entity(baos.toString("UTF-8")).build(); } /** * POST /api/v1/chaos will try a add a new event with the information in the url context, * ignoring the monkey probability and max termination configurations, for a specific instance group. * * @param content * the Json content passed to the http POST request * @return the response * @throws IOException */ @POST public Response addEvent(String content) throws IOException { ObjectMapper mapper = new ObjectMapper(); LOGGER.info(String.format("JSON content: '%s'", content)); JsonNode input = mapper.readTree(content); String eventType = getStringField(input, "eventType"); String groupType = getStringField(input, "groupType"); String groupName = getStringField(input, "groupName"); String chaosTypeName = getStringField(input, "chaosType"); ChaosType chaosType; if (!Strings.isNullOrEmpty(chaosTypeName)) { chaosType = ChaosType.parse(this.monkey.getChaosTypes(), chaosTypeName); } else { chaosType = new ShutdownInstanceChaosType(monkey.context().configuration()); } Response.Status responseStatus; ByteArrayOutputStream baos = new ByteArrayOutputStream(); JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos, JsonEncoding.UTF8); gen.writeStartObject(); gen.writeStringField("eventType", eventType); gen.writeStringField("groupType", groupType); gen.writeStringField("groupName", groupName); gen.writeStringField("chaosType", chaosType.getKey()); if (StringUtils.isEmpty(eventType) || StringUtils.isEmpty(groupType) || StringUtils.isEmpty(groupName)) { responseStatus = Response.Status.BAD_REQUEST; gen.writeStringField("message", "eventType, groupType, and groupName parameters are all required"); } else { if (eventType.equals("CHAOS_TERMINATION")) { responseStatus = addTerminationEvent(groupType, groupName, chaosType, gen); } else { responseStatus = Response.Status.BAD_REQUEST; gen.writeStringField("message", String.format("Unrecognized event type: %s", eventType)); } } gen.writeEndObject(); gen.close(); LOGGER.info("entity content is '{}'", baos.toString("UTF-8")); return Response.status(responseStatus).entity(baos.toString("UTF-8")).build(); } private Response.Status addTerminationEvent(String groupType, String groupName, ChaosType chaosType, JsonGenerator gen) throws IOException { LOGGER.info("Running on-demand termination for instance group type '{}' and name '{}'", groupType, groupName); Response.Status responseStatus; try { Event evt = monkey.terminateNow(groupType, groupName, chaosType); if (evt != null) { responseStatus = Response.Status.OK; gen.writeStringField("monkeyType", evt.monkeyType().name()); gen.writeStringField("eventId", evt.id()); gen.writeNumberField("eventTime", evt.eventTime().getTime()); gen.writeStringField("region", evt.region()); for (Map.Entry pair : evt.fields().entrySet()) { gen.writeStringField(pair.getKey(), pair.getValue()); } } else { responseStatus = Response.Status.INTERNAL_SERVER_ERROR; gen.writeStringField("message", String.format("Failed to terminate instance in group %s [type %s]", groupName, groupType)); } } catch (FeatureNotEnabledException e) { responseStatus = Response.Status.FORBIDDEN; gen.writeStringField("message", e.getMessage()); } catch (InstanceGroupNotFoundException e) { responseStatus = Response.Status.NOT_FOUND; gen.writeStringField("message", e.getMessage()); } catch (NotFoundException e) { // Available instance cannot be found to terminate, maybe the instance is already gone responseStatus = Response.Status.GONE; gen.writeStringField("message", e.getMessage()); } LOGGER.info("On-demand termination completed."); return responseStatus; } private String getStringField(JsonNode input, String field) { JsonNode node = input.get(field); if (node == null) { return null; } return node.getTextValue(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/resources/janitor/JanitorMonkeyResource.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.resources.janitor; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.MonkeyRunner; import com.netflix.simianarmy.janitor.JanitorMonkey; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.MappingJsonFactory; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Map; /** * The Class JanitorMonkeyResource for json REST apis. */ @Path("/v1/janitor") public class JanitorMonkeyResource { /** The Constant JSON_FACTORY. */ private static final MappingJsonFactory JSON_FACTORY = new MappingJsonFactory(); /** The monkey. */ private static JanitorMonkey monkey; /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(JanitorMonkeyResource.class); /** * Instantiates a janitor monkey resource with a specific janitor monkey. * * @param monkey * the janitor monkey */ public JanitorMonkeyResource(JanitorMonkey monkey) { JanitorMonkeyResource.monkey = monkey; } public JanitorMonkeyResource() { } public JanitorMonkey getJanitorMonkey() { if (JanitorMonkeyResource.monkey == null ) { JanitorMonkeyResource.monkey = MonkeyRunner.getInstance().factory(JanitorMonkey.class); } return monkey; } /** * GET /api/v1/janitor/addEvent will try to a add a new event with the information in the url query string. * This is the same as the regular POST addEvent except through a query string. This technically isn't * very REST-ful as it is a GET call that creates an Opt-out/in event, but is a convenience method * for exposing opt-in/opt-out functionality more directly, for example in an email notification. * * @param eventType eventType from the query string * @param resourceId resourceId from the query string * @return the response * @throws IOException */ @GET @Path("addEvent") public Response addEventThroughHttpGet( @QueryParam("eventType") String eventType, @QueryParam("resourceId") String resourceId, @QueryParam("region") String region) throws IOException { Response.Status responseStatus; ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write("".getBytes()); if (StringUtils.isEmpty(eventType) || StringUtils.isEmpty(resourceId)) { responseStatus = Response.Status.BAD_REQUEST; baos.write("

NOPE!

Janitor didn't get that: eventType and resourceId parameters are both required

".getBytes()); } else { ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos2, JsonEncoding.UTF8); gen.writeStartObject(); gen.writeStringField("eventType", eventType); gen.writeStringField("resourceId", resourceId); if (eventType.equals("OPTIN")) { responseStatus = optInResource(resourceId, true, region, gen); } else if (eventType.equals("OPTOUT")) { responseStatus = optInResource(resourceId, false, region, gen); } else { responseStatus = Response.Status.BAD_REQUEST; gen.writeStringField("message", String.format("Unrecognized event type: %s", eventType)); } gen.writeEndObject(); gen.close(); if(responseStatus == Response.Status.OK) { baos.write(("

SUCCESS!

Resource " + resourceId + " has been " + eventType + " of Janitor Monkey!

").getBytes()); } else { baos.write(("

NOPE!

Janitor is Confused! Error processing Resource " + resourceId + "

").getBytes()); } String jsonout = String.format("

Monkey JSON Response:

", baos2.toString()); baos.write(jsonout.getBytes()); } baos.write("".getBytes()); return Response.status(responseStatus).entity(baos.toString("UTF-8")).build(); } /** * POST /api/v1/janitor will try a add a new event with the information in the url context. * * @param content * the Json content passed to the http POST request * @return the response * @throws IOException */ @POST public Response addEvent(String content) throws IOException { ObjectMapper mapper = new ObjectMapper(); LOGGER.info(String.format("JSON content: '%s'", content)); JsonNode input = mapper.readTree(content); String eventType = getStringField(input, "eventType"); String resourceId = getStringField(input, "resourceId"); String region = getStringField(input, "region"); Response.Status responseStatus; ByteArrayOutputStream baos = new ByteArrayOutputStream(); JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos, JsonEncoding.UTF8); gen.writeStartObject(); gen.writeStringField("eventType", eventType); gen.writeStringField("resourceId", resourceId); if (StringUtils.isEmpty(eventType) || StringUtils.isEmpty(resourceId)) { responseStatus = Response.Status.BAD_REQUEST; gen.writeStringField("message", "eventType and resourceId parameters are all required"); } else { if (eventType.equals("OPTIN")) { responseStatus = optInResource(resourceId, true, region, gen); } else if (eventType.equals("OPTOUT")) { responseStatus = optInResource(resourceId, false, region, gen); } else { responseStatus = Response.Status.BAD_REQUEST; gen.writeStringField("message", String.format("Unrecognized event type: %s", eventType)); } } gen.writeEndObject(); gen.close(); LOGGER.info("entity content is '{}'", baos.toString("UTF-8")); return Response.status(responseStatus).entity(baos.toString("UTF-8")).build(); } /** * Gets the janitor status (e.g. to support an AWS ELB Healthcheck on an instance running JanitorMonkey). * Creates GET /api/v1/janitor api which responds 200 OK if JanitorMonkey is running. * * @param uriInfo * the uri info * @return the chaos events json response * @throws IOException * Signals that an I/O exception has occurred. */ @GET public Response getJanitorStatus(@Context UriInfo uriInfo) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); JsonGenerator gen = JSON_FACTORY.createJsonGenerator(baos, JsonEncoding.UTF8); gen.writeStartArray(); gen.writeStartObject(); gen.writeStringField("JanitorMonkeyStatus", "OnLikeDonkeyKong"); gen.writeEndObject(); gen.writeEndArray(); gen.close(); return Response.status(Response.Status.OK).entity(baos.toString("UTF-8")).build(); } private Response.Status optInResource(String resourceId, boolean optIn, String region, JsonGenerator gen) throws IOException { String op = optIn ? "in" : "out"; LOGGER.info(String.format("Opt %s resource %s for Janitor Monkey.", op, resourceId)); Response.Status responseStatus; Event evt; if (optIn) { evt = getJanitorMonkey().optInResource(resourceId, region); } else { evt = getJanitorMonkey().optOutResource(resourceId, region); } if (evt != null) { responseStatus = Response.Status.OK; gen.writeStringField("monkeyType", evt.monkeyType().name()); gen.writeStringField("eventId", evt.id()); gen.writeNumberField("eventTime", evt.eventTime().getTime()); gen.writeStringField("region", evt.region()); for (Map.Entry pair : evt.fields().entrySet()) { gen.writeStringField(pair.getKey(), pair.getValue()); } } else { responseStatus = Response.Status.INTERNAL_SERVER_ERROR; gen.writeStringField("message", String.format("Failed to opt %s resource %s", op, resourceId)); } LOGGER.info(String.format("Opt %s operation completed.", op)); return responseStatus; } private String getStringField(JsonNode input, String field) { JsonNode node = input.get(field); if (node == null) { return null; } return node.getTextValue(); } } ================================================ FILE: src/main/java/com/netflix/simianarmy/tunable/TunableInstanceGroup.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.tunable; import com.amazonaws.services.autoscaling.model.TagDescription; import com.netflix.simianarmy.GroupType; import com.netflix.simianarmy.basic.chaos.BasicInstanceGroup; import java.util.List; /** * Allows for individual InstanceGroups to alter the aggressiveness * of ChaosMonkey. * * @author jeffggardner * */ public class TunableInstanceGroup extends BasicInstanceGroup { public TunableInstanceGroup(String name, GroupType type, String region, List tags) { super(name, type, region, tags); } private double aggressionCoefficient = 1.0; /** * @return the aggressionCoefficient */ public final double getAggressionCoefficient() { return aggressionCoefficient; } /** * @param aggressionCoefficient the aggressionCoefficient to set */ public final void setAggressionCoefficient(double aggressionCoefficient) { this.aggressionCoefficient = aggressionCoefficient; } } ================================================ FILE: src/main/java/com/netflix/simianarmy/tunable/TunablyAggressiveChaosMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.tunable; import com.netflix.simianarmy.basic.chaos.BasicChaosMonkey; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; /** * This class modifies the probability by multiplying the configured * probability by the aggression coefficient tag on the instance group. * * @author jeffggardner */ public class TunablyAggressiveChaosMonkey extends BasicChaosMonkey { public TunablyAggressiveChaosMonkey(Context ctx) { super(ctx); } /** * Gets the tuned probability value, returns 0 if the group is not * enabled. Calls getEffectiveProbability and modifies that value if * the instance group is a TunableInstanceGroup. * * @param group The instance group * @return the effective probability value for the instance group */ @Override protected double getEffectiveProbability(InstanceGroup group) { if (!isGroupEnabled(group)) { return 0; } double probability = getEffectiveProbabilityFromCfg(group); // if this instance group is tunable, then factor in the aggression coefficient if (group instanceof TunableInstanceGroup ) { TunableInstanceGroup tunable = (TunableInstanceGroup) group; probability *= tunable.getAggressionCoefficient(); } return probability; } } ================================================ FILE: src/main/resources/chaos.properties ================================================ # The file contains the properties for Chaos Monkey. # see documentation at: # https://github.com/Netflix/SimianArmy/wiki/Configuration # let chaos run simianarmy.chaos.enabled = true # don't allow chaos to kill (ie dryrun mode) simianarmy.chaos.leashed = true # set to "false" for Opt-In behavior, "true" for Opt-Out behavior simianarmy.chaos.ASG.enabled = false # uncomment this line to use tunable aggression #simianarmy.client.chaos.class = com.netflix.simianarmy.tunable.TunablyAggressiveChaosMonkey # default probability for all ASGs simianarmy.chaos.ASG.probability = 1.0 # increase or decrease the termination limit simianarmy.chaos.ASG.maxTerminationsPerDay = 1.0 # Strategies simianarmy.chaos.shutdowninstance.enabled = true simianarmy.chaos.blockallnetworktraffic.enabled = false simianarmy.chaos.burncpu.enabled = false simianarmy.chaos.killprocesses.enabled = false simianarmy.chaos.nullroute.enabled = false simianarmy.chaos.failec2.enabled = false simianarmy.chaos.faildns.enabled = false simianarmy.chaos.faildynamodb.enabled = false simianarmy.chaos.fails3.enabled = false simianarmy.chaos.networkcorruption.enabled = false simianarmy.chaos.networklatency.enabled = false simianarmy.chaos.networkloss.enabled = false # Force-detaching EBS volumes may cause data loss simianarmy.chaos.detachvolumes.enabled = false # FillDisk fills the root disk. # NOTE: This may incur charges for an EBS root volume. See burnmoney option. simianarmy.chaos.burnio.enabled = false # BurnIO causes disk activity on the root disk. # NOTE: This may incur charges for an EBS root volume. See burnmoney option. simianarmy.chaos.filldisk.enabled = false # Where we know the chaos strategy will incur charges, we won't run it unless burnmoney is true. simianarmy.chaos.burnmoney = false # enable a specific ASG # simianarmy.chaos.ASG..enabled = true # simianarmy.chaos.ASG..probability = 1.0 # increase or decrease the termination limit for a specific ASG # simianarmy.chaos.ASG..maxTerminationsPerDay = 1.0 # Enroll in mandatory terminations. If a group has not had a # termination within the windowInDays range then it will terminate # one instance in the group with a 0.5 probability (at some point in # the next 2 days an instance should be terminated), then # do nothing again for windowInDays. This forces "enabled" groups # that have a probability of 0.0 to have terminations periodically. simianarmy.chaos.mandatoryTermination.enabled = false simianarmy.chaos.mandatoryTermination.windowInDays = 32 simianarmy.chaos.mandatoryTermination.defaultProbability = 0.5 # Enable notification for Chaos termination for a specific instance group # simianarmy.chaos...notification.enabled = true # Set the destination email the termination notification sent to for a specific instance group # simianarmy.chaos...ownerEmail = foo@bar.com # Set the source email that sends the termination notification # simianarmy.chaos.notification.sourceEmail = foo@bar.com # Enable notification for Chaos termination for all instance groups #simianarmy.chaos.notification.global.enabled = true # Set the destination email the termination notification is sent to for all instance groups #simianarmy.chaos.notification.global.receiverEmail = foo@bar.com # Set a prefix applied to the subject of all termination notifications # Probably want to include a trailing space to separate from start of default text #simianarmy.chaos.notification.subject.prefix = SubjectPrefix # Set a suffix applied to the subject of all termination notifications # Probably want to include an escaped space " \ " to separate from end of default text #simianarmy.chaos.notification.subject.suffix = \ SubjectSuffix # Set a prefix applied to the body of all termination notifications # Probably want to include a trailing space to separate from start of default text #simianarmy.chaos.notification.body.prefix = BodyPrefix # Set a suffix applied to the body of all termination notifications # Probably want to include an escaped space " \ " to separate from end of default text #simianarmy.chaos.notification.body.suffix = \ BodySuffix # Enable the email subject to be the same as the body, to include terminated instance and group information #simianarmy.chaos.notification.subject.isBody = true #set the tag filter on the ASGs to terminate only instances from the ASG with the this tag key and value #simianarmy.chaos.ASGtag.key = chaos_monkey #simianarmy.chaos.ASGtag.value = true ================================================ FILE: src/main/resources/client.properties ================================================ ##################################################################### ### Configure which client and context to use. ##################################################################### ### The default implementation is to use an AWS Client, equaling a property like the following: # #simianarmy.client.context.class=com.netflix.simianarmy.basic.BasicContext ### to use an VSphereClient instead, uncomment this: # #simianarmy.client.context.class=com.netflix.simianarmy.client.vsphere.VSphereContext # ### configure the specific selected client, e.g for VSphere these are # #simianarmy.client.vsphere.url=https://YOUR_VSPHERE_SERVER/sdk #simianarmy.client.vsphere.username=YOUR_SERVICE_ACCOUNT_USERNAME #simianarmy.client.vsphere.password=YOUR_SERVICE_ACCOUNT_PASSWORD ### configure the specific selected client, e.g for AWS these are ### both "accountKey" and "secretKey" can be left blank or be removed, ### if the credentials are provided as environment variable or ### an instance role is used to handle permissions ### see: http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-roles.html #simianarmy.client.aws.accountKey = fakeAccount #simianarmy.client.aws.secretKey = fakeSecret ### Comment out the following line to detect the AWS region where the instance is running simianarmy.client.aws.region = us-west-1 ### Common account name to make it easier to identify emails by subject simianarmy.client.aws.accountName = default ### To operate under an assumed role - the role will be assumed for all activity, sts:AssumeRole ### action must be allowed for the inital IAM role being used (long lived credentials) ### http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html # #simianarmy.client.aws.assumeRoleArn = arn:aws:iam::ACCOUNT:role/ROLE ### The VSpehere client uses a TerminationStrategy for killing VirtualMachines ### You can configure which property and value for it to set prior to resetting the VirtualMachine # #simianarmy.client.vsphere.terminationStrategy.property.name=Force Boot #simianarmy.client.vsphere.terminationStrategy.property.value=server # Uncomment to use a version of Monkey recorder that does not rely on AWS SDB #simianarmy.client.recorder.class=com.netflix.simianarmy.basic.LocalDbRecorder ### Operate in Cloud Formation mode - the random suffix appended to Auto Scaling Group names is ignored ### (specify ASG names as usual with no suffix in chaos.properties) # #simianarmy.client.chaos.class=com.netflix.simianarmy.basic.chaos.CloudFormationChaosMonkey # Use the following if a proxy is needed to connect to AWS APIs # proxyHost and proxyPort are required to connect through a proxy, proxyUser and proxyPassword are optional #simianarmy.client.aws.proxyHost= #simianarmy.client.aws.proxyPort= #simianarmy.client.aws.proxyUser= #simianarmy.client.aws.proxyPassword= ================================================ FILE: src/main/resources/conformity.properties ================================================ # let Conformity monkey run simianarmy.conformity.enabled = true # dryrun mode, no email notification to the owner of nonconforming clusters is sent simianarmy.conformity.leashed = true # By default Conformity Monkey wakes up every hour simianarmy.scheduler.frequency = 1 simianarmy.scheduler.frequencyUnit = HOURS simianarmy.scheduler.threads = 1 # Conformity Monkey runs every hour. simianarmy.calendar.openHour = 0 simianarmy.calendar.closeHour = 24 simianarmy.calendar.timezone = America/Los_Angeles # override to force monkey time, useful for debugging off hours #simianarmy.calendar.isMonkeyTime = true # Conformity monkey sends notifications to the owner of unconforming clusters between the open hour and close # hour only. In other hours, only summary email is sent. The default setting is to always send email notifications # after each run. simianarmy.conformity.notification.openHour = 0 simianarmy.conformity.notification.closeHour = 24 simianarmy.conformity.sdb.domain = SIMIAN_ARMY # The property below needs to be a valid email address to receive the summary email of Conformity Monkey # after each run simianarmy.conformity.summaryEmail.to = foo@bar.com # The property below needs to be a valid email address to send notifications for Conformity monkey simianarmy.conformity.notification.defaultEmail = foo@bar.com # The property below needs to be a valid email address to send notifications for Conformity Monkey simianarmy.conformity.notification.sourceEmail = foo@bar.com # By default Eureka is not enabled. The conformity rules that need to access Eureka are not added # when Eureka is not enabled. simianarmy.conformity.Eureka.enabled = false # The following property is used to enable the conformity rule to check whether there is mismatch of availability # zones between any auto scaling group and its ELBs in a cluster. simianarmy.conformity.rule.SameZonesInElbAndAsg.enabled = true # The following property is used to enable the conformity rule to check whether all instances in the cluster # are in required security groups. simianarmy.conformity.rule.InstanceInSecurityGroup.enabled = true # The following property specifies the required security groups in the InstanceInSecurityGroup conformity rule. simianarmy.conformity.rule.InstanceInSecurityGroup.requiredSecurityGroups = nf-infrastructure, nf-datacenter # The following property is used to enable the conformity rule to check whether there is any instance that is # older than certain days. simianarmy.conformity.rule.InstanceTooOld.enabled = true # The following property specifies the number of days used in the InstanceInSecurityGroup, any instance that is # old than this number of days is consider nonconforming. simianarmy.conformity.rule.InstanceTooOld.instanceAgeThreshold = 180 # The following property is used to enable the conformity rule to check whether all instances in the cluster # have a status url defined according to Discovery/Eureka. simianarmy.conformity.rule.InstanceHasStatusUrl.enabled = true # The following property is used to enable the conformity rule to check whether all instances in the cluster # have a health check url defined according to Discovery/Eureka. simianarmy.conformity.rule.InstanceHasHealthCheckUrl.enabled = true # The following property is used to enable the conformity rule to check whether there are unhealthy instances # in the cluster accoring to Discovery/Eureka. simianarmy.conformity.rule.InstanceIsHealthyInEureka.enabled = true # You can override a cluster's owner email by providing a property here. For example, the line below overrides # the owner email of cluster foo to foo@bar.com # simianarmy.conformity.cluster.foo.ownerEmail = foo@bar.com # You can exclude specific conformity rules for a cluster using this property. For example, the line below excludes # the conformity rule rule1 and rule2 on cluster foo. # simianarmy.conformity.cluster.foo.excludedRules = rule1,rule2 # You can opt out a cluster completely from Conformity Monkey by using this property. After a cluster is opted out, # no notification is sent for it no matter it is conforming or not. For example, the line below opts out the cluster # foo. # simianarmy.conformity.cluster.foo.optedOut = true ================================================ FILE: src/main/resources/janitor.properties ================================================ # see documentation at: # https://github.com/Netflix/SimianArmy/wiki/Configuration # By default Janitor Monkey wakes up every hour simianarmy.scheduler.frequency = 1 simianarmy.scheduler.frequencyUnit = HOURS simianarmy.scheduler.threads = 1 # Janitor Monkey runs every day at 11am. simianarmy.calendar.openHour = 11 simianarmy.calendar.closeHour = 11 simianarmy.calendar.timezone = America/Los_Angeles # Let Janitor Monkey run simianarmy.janitor.enabled = true # Don't allow Janitor Monkey to change resources (dryrun mode) simianarmy.janitor.leashed = true # The SDB domain for storing the resources managed by the Janitor Monkey. simianarmy.janitor.resources.sdb.domain = SIMIAN_ARMY # override to force monkey time, useful for debugging off hours #simianarmy.calendar.isMonkeyTime = true # Currently Janitor Monkey can clean up the following resources simianarmy.janitor.enabledResources = Instance, ASG, EBS_Volume, EBS_Snapshot, Launch_Config # The property below needs to be a valid email address to send notifications for Janitor Monkey simianarmy.janitor.notification.sourceEmail = foo@bar.com # The property below needs to be a valid email address to receive the summary email of Janitor Monkey # after each run simianarmy.janitor.summaryEmail.to = foo@bar.com # The property below needs to be a valid email address to receive the notifications of Janitor Monkey # for resouces that do not have a valid owner email specified simianarmy.janitor.notification.defaultEmail = foo@bar.com # The property below specifies the number of business days that a notification is sent before the # expected termination time. For example, if a resource is scheduled to be cleaned up by Janitor # Monkey on 12/13/2012, Thursday and the property is set to 2, the owner will receive notification # about the cleanup on 12/11/2012, Tuesday, which is 2 business days before the termination date. simianarmy.janitor.notification.daysBeforeTermination = 2 # The owner id that snapshots have for being managed by Janitor Monkey. This property needs # to set if you use Edda for getting snapshots to avoid getting shared snapshots. If you are using # AWS to get the snapshots, this property does not need to be set. #simianarmy.janitor.snapshots.ownerId = 012345678 # The following properties are used by the Janitor rule for cleaning up orphaned instances, # i.e. instances that are not in an auto-scaling group. simianarmy.janitor.rule.orphanedInstanceRule.enabled = true # An orphaned instance is marked as cleanup candidate if it has launched for more than the number # of days specified in the property below. simianarmy.janitor.rule.orphanedInstanceRule.instanceAgeThreshold = 2 # The number of business days the instance is kept after a notification is sent for the termination # when the instance has an owner. simianarmy.janitor.rule.orphanedInstanceRule.retentionDaysWithOwner = 3 # The number of business days the instance is kept after a notification is sent for the termination # when the instance has no owner. simianarmy.janitor.rule.orphanedInstanceRule.retentionDaysWithoutOwner = 8 # If true, don't consider members of an OpsWorks stack as orphans simianarmy.janitor.rule.orphanedInstanceRule.opsworks.parentage = false # The following properties are used by the Janitor rule for cleaning up untagged resources, # i.e. instances that are missing any required tags simianarmy.janitor.rule.untaggedRule.enabled = false # List of tags that are required for each resource simianarmy.janitor.rule.untaggedRule.requiredTags = owner, purpose, project # List of resource types that require tags simianarmy.janitor.rule.untaggedRule.resources = Instance, ASG, EBS_Volume, EBS_Snapshot # The number of business days the resource is kept after a notification is sent for the deletion # when the resource has an owner. simianarmy.janitor.rule.untaggedRule.retentionDaysWithOwner = 2 # The number of business days the resource is kept after a notification is sent for the deletion # when the resource has no owner. simianarmy.janitor.rule.untaggedRule.retentionDaysWithoutOwner = 2 # The following properties are used by the Janitor rule for cleaning up volumes that have been # detached from instances for certain days. simianarmy.janitor.rule.oldDetachedVolumeRule.enabled = true # A volume is considered a cleanup candidate after being detached for the number of days specified # in the property below. simianarmy.janitor.rule.oldDetachedVolumeRule.detachDaysThreshold = 30 # The number of business days the volume is kept after a notification is sent for the termination. simianarmy.janitor.rule.oldDetachedVolumeRule.retentionDays = 7 # The following properties are used by the Janitor rule for cleaning up volumes that should have been # deleted by AWS when the attached instance was terminated. This rule can be enabled only if Edda # is enabled since Janitor Monkey needs to query the history of the attached instance. simianarmy.janitor.rule.deleteOnTerminationRule.enabled = true # The number of business days the volume is kept after a notification is sent for the termination. simianarmy.janitor.rule.deleteOnTerminationRule.retentionDays = 3 # The following properties are used by the Janitor rule for cleaning up snapshots that have no existing # images generated from them and launched for certain days. simianarmy.janitor.rule.noGeneratedAMIRule.enabled = true # A snapshot without an image is considered a cleanup candidate after launching for the number of # days specified in the property below. simianarmy.janitor.rule.noGeneratedAMIRule.ageThreshold = 30 # The number of business days the snapshot is kept after a notification is sent for the termination. simianarmy.janitor.rule.noGeneratedAMIRule.retentionDays = 7 # The following properties are used by the Janitor rule for cleaning up auto-scaling groups that have # no active instances and the launch configuration is older than certain days. simianarmy.janitor.rule.oldEmptyASGRule.enabled = true # An an auto-scaling group without active instances is considered a cleanup candidate when its launch # configuration is older than the number of days specified in the property below. simianarmy.janitor.rule.oldEmptyASGRule.launchConfigAgeThreshold = 50 # The number of business days the auto-scaling group is kept after a notification is sent for the termination. simianarmy.janitor.rule.oldEmptyASGRule.retentionDays = 10 # The following properties are used by the Janitor rule for cleaning up auto-scaling groups that have # no active instances and have been suspended from the associated ELB traffic for certain days. simianarmy.janitor.rule.suspendedASGRule.enabled = true # An auto-scaling group without active instances is considered a cleanup candidate when it has been # suspended from the associated ELB traffic for the number of days specified in the property below. simianarmy.janitor.rule.suspendedASGRule.suspensionAgeThreshold = 2 # The number of business days the auto-scaling group is kept after a notification is sent for the termination. simianarmy.janitor.rule.suspendedASGRule.retentionDays = 5 # The property below specifies whether or not Eureka/Discovery is available for Janitor monkey to use. # Discovery/Eureka is used in the rules for cleaning up auto-scaling groups to decide if an auto-scaling group # has an 'active' instance, i.e. an instance that is registered and up in Discovery/Eureka. simianarmy.janitor.Eureka.enabled = false # The following properties are used by the Janitor rule for cleaning up launch configurations that are not # used by any auto scaling group and are older than certain days. simianarmy.janitor.rule.oldUnusedLaunchConfigRule.enabled = true # An unused launch configuration is considered a cleanup candidate when it is older than the number of days # specified in the property below. simianarmy.janitor.rule.oldUnusedLaunchConfigRule.ageThreshold = 4 # The number of business days the launch configuration is kept after a notification is sent for the termination. simianarmy.janitor.rule.oldUnusedLaunchConfigRule.retentionDays = 3 # The property below specifies the number of days to look back in the history when crawling the last reference # information of the images. simianarmy.janitor.image.crawler.lookBackDays = 60 # The owner id that images have for being managed by Janitor Monkey. #simianarmy.janitor.image.ownerId = 1234567890 # The following properties are used by the Janitor rule for cleaning up images that are not # used by any instance or launch configuration, and not used to create other images for more than certain days. # This rule is by default disabled, you need to have Edda running and enabled for using this rule. simianarmy.janitor.rule.unusedImageRule.enabled = false # An unused image is considered a cleanup candidate when it is not referenced for than the number of days # specified in the property below. simianarmy.janitor.rule.unusedImageRule.lastReferenceDaysThreshold = 45 # The number of business days the image is kept after a notification is sent for the termination. simianarmy.janitor.rule.unusedImageRule.retentionDays = 3 # You can enable Edda for Janitor Monkey to get better performance and accuracy. You need to start Edda first # to be able to use it. # simianarmy.janitor.edda.enabled = true # The Edda endpoint in each region you need Janitor Monkey #simianarmy.janitor.edda.endpoint.us-east-1 = http://localhost:8080 # The Edda client configurations. # simianarmy.janitor.edda.client.timeout = 30000 # simianarmy.janitor.edda.client.retries = 3 # simianarmy.janitor.edda.client.retryInterval = 1000 # Append domain if owner tag has no domain # simianarmy.janitor.notification.ownerEmailDomain = bar.com ================================================ FILE: src/main/resources/log4j.properties ================================================ log4j.rootLogger=INFO, stdout # stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} - %-5p %C{1} - [%F:%L] %m%n ================================================ FILE: src/main/resources/scripts/burncpu.sh ================================================ #!/bin/bash # Script for BurnCpu Chaos Monkey cat << EOF > /tmp/infiniteburn.sh #!/bin/bash while true; do openssl speed; done EOF # 32 parallel 100% CPU tasks should hit even the biggest EC2 instances for i in {1..32} do nohup /bin/bash /tmp/infiniteburn.sh & done ================================================ FILE: src/main/resources/scripts/burnio.sh ================================================ #!/bin/bash # Script for BurnIO Chaos Monkey cat << EOF > /tmp/loopburnio.sh #!/bin/bash while true; do dd if=/dev/urandom of=/burn bs=1M count=1024 iflag=fullblock done EOF nohup /bin/bash /tmp/loopburnio.sh & ================================================ FILE: src/main/resources/scripts/faildns.sh ================================================ #!/bin/bash # Script for FailDns Chaos Monkey # Block all traffic on port 53 iptables -A INPUT -p tcp -m tcp --dport 53 -j DROP iptables -A INPUT -p udp -m udp --dport 53 -j DROP ================================================ FILE: src/main/resources/scripts/faildynamodb.sh ================================================ #!/bin/bash # Script for FailDynamoDb Chaos Monkey # Block well-known Amazon DynamoDB API endpoints echo "127.0.0.1 dynamodb.us-east-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 dynamodb.us-northeast-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 dynamodb.us-gov-west-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 dynamodb.us-west-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 dynamodb.us-west-2.amazonaws.com" >> /etc/hosts echo "127.0.0.1 dynamodb.sa-east-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 dynamodb.ap-southeast-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 dynamodb.ap-southeast-2.amazonaws.com" >> /etc/hosts echo "127.0.0.1 dynamodb.eu-west-1.amazonaws.com" >> /etc/hosts ================================================ FILE: src/main/resources/scripts/failec2.sh ================================================ #!/bin/bash # Script for FailEc2 Chaos Monkey # Block well-known Amazon EC2 API endpoints echo "127.0.0.1 ec2.us-east-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 ec2.us-northeast-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 ec2.us-gov-west-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 ec2.us-west-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 ec2.us-west-2.amazonaws.com" >> /etc/hosts echo "127.0.0.1 ec2.sa-east-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 ec2.ap-southeast-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 ec2.ap-southeast-2.amazonaws.com" >> /etc/hosts echo "127.0.0.1 ec2.eu-west-1.amazonaws.com" >> /etc/hosts ================================================ FILE: src/main/resources/scripts/fails3.sh ================================================ #!/bin/bash # Script for FailS3 Chaos Monkey # See http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region echo "127.0.0.1 s3.amazonaws.com" >> /etc/hosts echo "127.0.0.1 s3-external-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 s3-us-west-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 s3-us-west-2.amazonaws.com" >> /etc/hosts echo "127.0.0.1 s3-eu-west-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 s3-ap-southeast-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 s3-ap-southeast-2.amazonaws.com" >> /etc/hosts echo "127.0.0.1 s3-ap-northeast-1.amazonaws.com" >> /etc/hosts echo "127.0.0.1 s3-sa-east-1.amazonaws.com" >> /etc/hosts ================================================ FILE: src/main/resources/scripts/filldisk.sh ================================================ #!/bin/bash # Script for FillDisk Chaos Monkey # 65 GB should be enough to fill up all EC2 root disks! nohup dd if=/dev/urandom of=/burn bs=1M count=65536 iflag=fullblock & ================================================ FILE: src/main/resources/scripts/killprocesses.sh ================================================ #!/bin/bash # Script for KillProcesses Chaos Monkey cat << EOF > /tmp/kill_loop.sh #!/bin/bash while true; do pkill -KILL -f java pkill -KILL -f python sleep 1 done EOF nohup /bin/bash /tmp/kill_loop.sh & ================================================ FILE: src/main/resources/scripts/networkcorruption.sh ================================================ #!/bin/bash # Script for NetworkCorruption Chaos Monkey # Corrupts 5% of packets sudo tc qdisc add dev eth0 root netem corrupt 5% ================================================ FILE: src/main/resources/scripts/networklatency.sh ================================================ #!/bin/bash # Script for NetworkLatency Chaos Monkey # Adds 1000ms +- 500ms of latency to each packet sudo tc qdisc add dev eth0 root netem delay 1000ms 500ms ================================================ FILE: src/main/resources/scripts/networkloss.sh ================================================ #!/bin/bash # Script for NetworkLoss Chaos Monkey # Drops 7% of packets, with 25% correlation with previous packet loss # 7% is high, but it isn't high enough that TCP will fail entirely sudo tc qdisc add dev eth0 root netem loss 7% 25% ================================================ FILE: src/main/resources/scripts/nullroute.sh ================================================ #!/bin/bash # Script for NullRoute Chaos Monkey ip route add blackhole 10.0.0.0/8 ================================================ FILE: src/main/resources/simianarmy.properties ================================================ # see documentation at: # https://github.com/Netflix/SimianArmy/wiki/Configuration simianarmy.recorder.sdb.domain = SIMIAN_ARMY # If using a non-SimbleDB recorder (LocalDB), these settings tweak defaults. # Following should be a writeable location, for monkey events when SimpleDB is not used #simianarmy.recorder.localdb.file=/tmp/simianarmy_events # Max number of events to record; old events will be expired after this limit is # reached. Use this to avoid filling disk with events (or attach a big volume!) #simianarmy.recorder.localdb.max_events=1000000 # Optional password to encrypt event storage. #simianarmy.recorder.localdb.password=some_secret simianarmy.scheduler.frequency = 1 simianarmy.scheduler.frequencyUnit = HOURS simianarmy.scheduler.threads = 1 simianarmy.calendar.openHour = 9 simianarmy.calendar.closeHour = 15 simianarmy.calendar.timezone = America/Los_Angeles # override to force monkey time, useful for debugging off hours #simianarmy.calendar.isMonkeyTime = true # Allows you to Set the (case sensitive) AWS Tag Key to use for owner tags; e.g. Owner or owner # Will be Monkey Wide - used by all Monkeys. If not set defaults to "owner" # simianarmy.tags.owner = Owner # Region override for Amazon Simple Email Service Client #simianarmy.aws.email.region=us-west-1 ================================================ FILE: src/main/resources/volumeTagging.properties ================================================ # see documentation at: # https://github.com/Netflix/SimianArmy/wiki/Configuration # The properties in this file are used by the monkey that tags volumes with information that # Janitor Monkey will need for cleaning up volumes. # Let the monkey run. simianarmy.volumeTagging.enabled = true # Running in the dryrun mode, no tagging is really done. simianarmy.volumeTagging.leashed = true # Set the property below if you need the owner alias to be converted to a valid email address #simianarmy.volumeTagging.ownerEmailDomain = foo.com # The volume tagging monkey always runs. The tagging process is needed by the Janitor Monkey to # clean up volumes. We can keep the volume tagging monkey running so we don't miss any change # of volumes. simianarmy.calendar.isMonkeyTime = true ================================================ FILE: src/main/webapp/WEB-INF/web.xml ================================================ Simian Army Monkey Server com.netflix.simianarmy.basic.BasicMonkeyServer 1 jersey-servlet com.sun.jersey.spi.container.servlet.ServletContainer com.sun.jersey.config.property.packages com.netflix.simianarmy.resources 2 jersey-servlet /api/* ================================================ FILE: src/test/java/com/netflix/simianarmy/TestMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy; import org.testng.annotations.Test; import org.testng.Assert; public class TestMonkey extends Monkey { public TestMonkey() { super(new TestMonkeyContext(Type.TEST)); } public enum Type implements MonkeyType { TEST }; public Type type() { return Type.TEST; } public void doMonkeyBusiness() { Assert.assertTrue(true, "ran monkey business"); } @Test public void testStart() { this.start(); } @Test public void testStop() { this.stop(); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/TestMonkeyContext.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import com.netflix.simianarmy.MonkeyRecorder.Event; import com.netflix.simianarmy.basic.BasicConfiguration; import com.netflix.simianarmy.basic.BasicRecorderEvent; import org.jclouds.compute.ComputeService; import org.jclouds.domain.LoginCredentials; import org.jclouds.ssh.SshClient; import org.testng.Assert; import java.util.*; import java.util.concurrent.TimeUnit; public class TestMonkeyContext implements Monkey.Context { private final MonkeyType monkeyType; private final LinkedList eventReport = new LinkedList(); public TestMonkeyContext(MonkeyType monkeyType) { this.monkeyType = monkeyType; } @Override public MonkeyConfiguration configuration() { return new BasicConfiguration(new Properties()); } @Override public MonkeyScheduler scheduler() { return new MonkeyScheduler() { @Override public int frequency() { return 1; } @Override public TimeUnit frequencyUnit() { return TimeUnit.HOURS; } @Override public void start(Monkey monkey, Runnable run) { Assert.assertEquals(monkey.type().name(), monkeyType.name(), "starting monkey"); run.run(); } @Override public void stop(Monkey monkey) { Assert.assertEquals(monkey.type().name(), monkeyType.name(), "stopping monkey"); } }; } @Override public MonkeyCalendar calendar() { // CHECKSTYLE IGNORE MagicNumberCheck return new MonkeyCalendar() { @Override public boolean isMonkeyTime(Monkey monkey) { return true; } @Override public int openHour() { return 10; } @Override public int closeHour() { return 11; } @Override public Calendar now() { return Calendar.getInstance(); } @Override public Date getBusinessDay(Date date, int n) { throw new RuntimeException("Not implemented."); } }; } @Override public CloudClient cloudClient() { return new CloudClient() { @Override public void terminateInstance(String instanceId) { } @Override public void createTagsForResources(Map keyValueMap, String... resourceIds) { } @Override public void deleteAutoScalingGroup(String asgName) { } @Override public void deleteVolume(String volumeId) { } @Override public void deleteSnapshot(String snapshotId) { } @Override public void deleteImage(String imageId) { } @Override public void deleteElasticLoadBalancer(String elbId) { } @Override public void deleteDNSRecord(String dnsname, String dnstype, String hostedzoneid) { } @Override public void deleteLaunchConfiguration(String launchConfigName) { } @Override public List listAttachedVolumes(String instanceId, boolean includeRoot) { throw new UnsupportedOperationException(); } @Override public void detachVolume(String instanceId, String volumeId, boolean force) { throw new UnsupportedOperationException(); } @Override public ComputeService getJcloudsComputeService() { throw new UnsupportedOperationException(); } @Override public String getJcloudsId(String instanceId) { throw new UnsupportedOperationException(); } @Override public SshClient connectSsh(String instanceId, LoginCredentials credentials) { throw new UnsupportedOperationException(); } @Override public String findSecurityGroup(String instanceId, String groupName) { throw new UnsupportedOperationException(); } @Override public String createSecurityGroup(String instanceId, String groupName, String description) { throw new UnsupportedOperationException(); } @Override public boolean canChangeInstanceSecurityGroups(String instanceId) { throw new UnsupportedOperationException(); } @Override public void setInstanceSecurityGroups(String instanceId, List groupIds) { throw new UnsupportedOperationException(); } }; } private final MonkeyRecorder recorder = new MonkeyRecorder() { private final List events = new LinkedList(); @Override public Event newEvent(MonkeyType mkType, EventType eventType, String region, String id) { return new BasicRecorderEvent(mkType, eventType, region, id); } @Override public void recordEvent(Event evt) { events.add(evt); } @Override public List findEvents(Map query, Date after) { return events; } @Override public List findEvents(MonkeyType mkeyType, Map query, Date after) { // used from BasicScheduler return events; } @Override public List findEvents(MonkeyType mkeyType, EventType eventType, Map query, Date after) { // used from ChaosMonkey List evts = new LinkedList(); for (Event evt : events) { if (query.get("groupName").equals(evt.field("groupName")) && evt.monkeyType() == mkeyType && evt.eventType() == eventType && evt.eventTime().after(after)) { evts.add(evt); } } return evts; } }; @Override public MonkeyRecorder recorder() { return recorder; } @Override public void reportEvent(Event evt) { eventReport.add(evt); } @Override public void resetEventReport() { eventReport.clear(); } @Override public String getEventReport() { StringBuilder report = new StringBuilder(); for (Event event : eventReport) { report.append(event.eventType()); report.append(" "); report.append(event.id()); } return report.toString(); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/TestMonkeyRunner.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy; import java.util.List; import org.testng.annotations.Test; import org.testng.Assert; public class TestMonkeyRunner { private static boolean monkeyARan = false; private static class MonkeyA extends TestMonkey { public void doMonkeyBusiness() { monkeyARan = true; } } private static boolean monkeyBRan = false; private static class MonkeyB extends Monkey { public enum Type implements MonkeyType { B }; public Type type() { return Type.B; } public interface Context extends Monkey.Context { boolean getTrue(); } private Context ctx; public MonkeyB(Context ctx) { super(ctx); this.ctx = ctx; } public void doMonkeyBusiness() { monkeyBRan = ctx.getTrue(); } } private static class MonkeyBContext extends TestMonkeyContext implements MonkeyB.Context { public MonkeyBContext() { super(MonkeyB.Type.B); } public boolean getTrue() { return true; } } @Test void testInstance() { Assert.assertEquals(MonkeyRunner.getInstance(), MonkeyRunner.INSTANCE); } @Test void testRunner() { MonkeyRunner runner = MonkeyRunner.getInstance(); runner.addMonkey(MonkeyA.class); runner.replaceMonkey(MonkeyA.class); runner.addMonkey(MonkeyB.class, MonkeyBContext.class); runner.replaceMonkey(MonkeyB.class, MonkeyBContext.class); List monkeys = runner.getMonkeys(); Assert.assertEquals(monkeys.size(), 2); Assert.assertEquals(monkeys.get(0).type().name(), "TEST"); Assert.assertEquals(monkeys.get(1).type().name(), "B"); Monkey a = runner.factory(MonkeyA.class); Assert.assertEquals(a.type().name(), "TEST"); Monkey b = runner.factory(MonkeyB.class, MonkeyBContext.class); Assert.assertEquals(b.type().name(), "B"); Assert.assertNull(runner.getContextClass(MonkeyA.class)); Assert.assertEquals(runner.getContextClass(MonkeyB.class), MonkeyBContext.class); runner.start(); Assert.assertEquals(monkeyARan, true, "monkeyA ran"); Assert.assertEquals(monkeyBRan, true, "monkeyB ran"); runner.stop(); runner.removeMonkey(MonkeyA.class); Assert.assertEquals(monkeys.size(), 1); Assert.assertEquals(monkeys.get(0).type().name(), "B"); runner.removeMonkey(MonkeyB.class); Assert.assertEquals(monkeys.size(), 0); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/TestUtils.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy; import static org.joda.time.DateTimeConstants.MILLIS_PER_DAY; import org.joda.time.DateTime; import org.testng.Assert; /** Utility class for test cases. * @author mgeis * */ public final class TestUtils { private TestUtils() { //this should never be called //if called internally, throw an error throw new InstantiationError("Instantiation of TestUtils utility class prohibited."); } /** Verify that the termination date is roughly retentionDays from now * By 'roughly' we mean within one day. There are times (twice per year) * when certain tests execute and the Daylight Savings cutover makes it not * a precisely rounded day amount (for example, a termination policy of 4 days * will really be about 3.95 days, or 95 hours, because one hour is lost as * the clocks "spring ahead"). * * A more precise, but complicated logic could be written to make sure that "roughly" * means not more than an hour before and not more than an hour after the anticipated * cutoff, but that makes the test much less readable. * * By just making sure that the difference between the actual and proposed dates * is less than one day, we get a rough idea of whether the termination time was correct. * @param resource The AWS Resource to be checked * @param retentionDays number of days it should be kept around * @param timeOfCheck The time the check is run */ public static void verifyTerminationTimeRough(Resource resource, int retentionDays, DateTime timeOfCheck) { long days = (resource.getExpectedTerminationTime().getTime() - timeOfCheck.getMillis()) / MILLIS_PER_DAY; Assert.assertTrue(Math.abs(days - retentionDays) <= 1); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/TestAWSEmailNotifier.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.aws; import org.testng.Assert; import org.testng.annotations.Test; // CHECKSTYLE IGNORE MagicNumberCheck public class TestAWSEmailNotifier extends AWSEmailNotifier { public TestAWSEmailNotifier() { super(null); } @Override public String buildEmailSubject(String to) { return null; } @Override public String[] getCcAddresses(String to) { return new String[0]; } @Override public String getSourceAddress(String to) { return null; } @Test public void testEmailWithHashIsValid() { TestAWSEmailNotifier emailNotifier = new TestAWSEmailNotifier(); Assert.assertTrue(emailNotifier.isValidEmail("#bla-#name@domain-test.com"), "Email with hash is not valid"); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/TestRDSRecorder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.Matchers; import org.mockito.Mockito; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.MonkeyType; import com.netflix.simianarmy.basic.BasicRecorderEvent; // CHECKSTYLE IGNORE MagicNumberCheck public class TestRDSRecorder extends RDSRecorder { private static final String REGION = "us-west-1"; public TestRDSRecorder() { super(mock(JdbcTemplate.class), "recordertable", REGION); } public enum Type implements MonkeyType { MONKEY } public enum EventTypes implements EventType { EVENT } @Test public void testInit() { TestRDSRecorder recorder = new TestRDSRecorder(); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); Mockito.doNothing().when(recorder.getJdbcTemplate()).execute(sqlCap.capture()); recorder.init(); 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) )"); } @SuppressWarnings("unchecked") @Test public void testInsertNewRecordEvent() { // mock the select query that is issued to see if the record already exists ArrayList events = new ArrayList<>(); TestRDSRecorder recorder = new TestRDSRecorder(); when(recorder.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(events); Event evt = newEvent(Type.MONKEY, EventTypes.EVENT, "region", "testId"); evt.addField("field1", "value1"); evt.addField("field2", "value2"); // this will be ignored as it conflicts with reserved key evt.addField("id", "ignoreThis"); ArgumentCaptor objCap = ArgumentCaptor.forClass(Object.class); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); when(recorder.getJdbcTemplate().update(sqlCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture())).thenReturn(1); recorder.recordEvent(evt); List args = objCap.getAllValues(); Assert.assertEquals(sqlCap.getValue(), "insert into recordertable (eventId,eventTime,monkeyType,eventType,region,dataJson) values (?,?,?,?,?,?)"); Assert.assertEquals(args.size(), 6); Assert.assertEquals(args.get(0).toString(), evt.id()); Assert.assertEquals(args.get(1).toString(), evt.eventTime().getTime() + ""); Assert.assertEquals(args.get(2).toString(), SimpleDBRecorder.enumToValue(evt.monkeyType())); Assert.assertEquals(args.get(3).toString(), SimpleDBRecorder.enumToValue(evt.eventType())); Assert.assertEquals(args.get(4).toString(), evt.region()); } private Event mkSelectResult(String id, Event evt) { evt.addField("field1", "value1"); evt.addField("field2", "value2"); return evt; } @SuppressWarnings("unchecked") @Test public void testFindEvent() { Event evt1 = new BasicRecorderEvent(Type.MONKEY, EventTypes.EVENT, "region", "testId1", 1330538400000L); mkSelectResult("testId1", evt1); Event evt2 = new BasicRecorderEvent(Type.MONKEY, EventTypes.EVENT, "region", "testId2", 1330538400000L); mkSelectResult("testId2", evt2); ArrayList events = new ArrayList<>(); TestRDSRecorder recorder = new TestRDSRecorder(); events.add(evt1); events.add(evt2); when(recorder.getJdbcTemplate().query(Matchers.anyString(), Matchers.argThat(new ArgumentMatcher(){ @Override public boolean matches(Object argument) { Object [] args = (Object [])argument; Assert.assertTrue(args[0] instanceof String); Assert.assertEquals((String)args[0],REGION); return true; } }), Matchers.any(RowMapper.class))).thenReturn(events); Map query = new LinkedHashMap(); query.put("instanceId", "testId1"); verifyEvents(recorder.findEvents(query, new Date(0))); } void verifyEvents(List events) { Assert.assertEquals(events.size(), 2); Assert.assertEquals(events.get(0).id(), "testId1"); Assert.assertEquals(events.get(0).eventTime().getTime(), 1330538400000L); Assert.assertEquals(events.get(0).monkeyType(), Type.MONKEY); Assert.assertEquals(events.get(0).eventType(), EventTypes.EVENT); Assert.assertEquals(events.get(0).field("field1"), "value1"); Assert.assertEquals(events.get(0).field("field2"), "value2"); Assert.assertEquals(events.get(0).fields().size(), 2); Assert.assertEquals(events.get(1).id(), "testId2"); Assert.assertEquals(events.get(1).eventTime().getTime(), 1330538400000L); Assert.assertEquals(events.get(1).monkeyType(), Type.MONKEY); Assert.assertEquals(events.get(1).eventType(), EventTypes.EVENT); Assert.assertEquals(events.get(1).field("field1"), "value1"); Assert.assertEquals(events.get(1).field("field2"), "value2"); Assert.assertEquals(events.get(1).fields().size(), 2); } @SuppressWarnings("unchecked") @Test public void testFindEventNotFound() { ArrayList events = new ArrayList<>(); TestRDSRecorder recorder = new TestRDSRecorder(); when(recorder.getJdbcTemplate().query(Matchers.anyString(), Matchers.argThat(new ArgumentMatcher(){ @Override public boolean matches(Object argument) { Object [] args = (Object [])argument; Assert.assertTrue(args[0] instanceof String); Assert.assertEquals((String)args[0],REGION); return true; } }), Matchers.any(RowMapper.class))).thenReturn(events); List results = recorder.findEvents(new HashMap(), new Date()); Assert.assertEquals(results.size(), 0); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/TestSimpleDBRecorder.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.mockito.ArgumentCaptor; import org.testng.Assert; import org.testng.annotations.Test; import com.amazonaws.services.simpledb.AmazonSimpleDB; import com.amazonaws.services.simpledb.model.Attribute; import com.amazonaws.services.simpledb.model.Item; import com.amazonaws.services.simpledb.model.PutAttributesRequest; import com.amazonaws.services.simpledb.model.ReplaceableAttribute; import com.amazonaws.services.simpledb.model.SelectRequest; import com.amazonaws.services.simpledb.model.SelectResult; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.MonkeyType; import com.netflix.simianarmy.client.aws.AWSClient; // CHECKSTYLE IGNORE MagicNumberCheck public class TestSimpleDBRecorder extends SimpleDBRecorder { private static AWSClient makeMockAWSClient() { AmazonSimpleDB sdbMock = mock(AmazonSimpleDB.class); AWSClient awsClient = mock(AWSClient.class); when(awsClient.sdbClient()).thenReturn(sdbMock); when(awsClient.region()).thenReturn("region"); return awsClient; } public TestSimpleDBRecorder() { super(makeMockAWSClient(), "DOMAIN"); sdbMock = super.sdbClient(); } private final AmazonSimpleDB sdbMock; @Override protected AmazonSimpleDB sdbClient() { return sdbMock; } protected AmazonSimpleDB superSdbClient() { return super.sdbClient(); } @Test public void testClients() { TestSimpleDBRecorder recorder1 = new TestSimpleDBRecorder(); Assert.assertNotNull(recorder1.superSdbClient(), "non null super sdbClient"); TestSimpleDBRecorder recorder2 = new TestSimpleDBRecorder(); Assert.assertNotNull(recorder2.superSdbClient(), "non null super sdbClient"); } public enum Type implements MonkeyType { MONKEY } public enum EventTypes implements EventType { EVENT } @Test public void testRecordEvent() { ArgumentCaptor arg = ArgumentCaptor.forClass(PutAttributesRequest.class); Event evt = newEvent(Type.MONKEY, EventTypes.EVENT, "region", "testId"); evt.addField("field1", "value1"); evt.addField("field2", "value2"); // this will be ignored as it conflicts with reserved key evt.addField("id", "ignoreThis"); recordEvent(evt); verify(sdbMock).putAttributes(arg.capture()); PutAttributesRequest req = arg.getValue(); Assert.assertEquals(req.getDomainName(), "DOMAIN"); Assert.assertEquals(req.getItemName(), "MONKEY-testId-region-" + evt.eventTime().getTime()); Map map = new HashMap(); for (ReplaceableAttribute attr : req.getAttributes()) { map.put(attr.getName(), attr.getValue()); } Assert.assertEquals(map.remove("id"), "testId"); Assert.assertEquals(map.remove("eventTime"), String.valueOf(evt.eventTime().getTime())); Assert.assertEquals(map.remove("region"), "region"); Assert.assertEquals(map.remove("recordType"), "MonkeyEvent"); Assert.assertEquals(map.remove("monkeyType"), "MONKEY|com.netflix.simianarmy.aws.TestSimpleDBRecorder$Type"); Assert.assertEquals(map.remove("eventType"), "EVENT|com.netflix.simianarmy.aws.TestSimpleDBRecorder$EventTypes"); Assert.assertEquals(map.remove("field1"), "value1"); Assert.assertEquals(map.remove("field2"), "value2"); Assert.assertEquals(map.size(), 0); } private SelectResult mkSelectResult(String id) { Item item = new Item(); List attrs = new LinkedList(); attrs.add(new Attribute("id", id)); attrs.add(new Attribute("eventTime", "1330538400000")); attrs.add(new Attribute("region", "region")); attrs.add(new Attribute("recordType", "MonkeyEvent")); attrs.add(new Attribute("monkeyType", "MONKEY|com.netflix.simianarmy.aws.TestSimpleDBRecorder$Type")); attrs.add(new Attribute("eventType", "EVENT|com.netflix.simianarmy.aws.TestSimpleDBRecorder$EventTypes")); attrs.add(new Attribute("field1", "value1")); attrs.add(new Attribute("field2", "value2")); item.setAttributes(attrs); item.setName("MONKEY-" + id + "-region"); SelectResult result = new SelectResult(); result.setItems(Arrays.asList(item)); return result; } @Test public void testFindEvent() { SelectResult result1 = mkSelectResult("testId1"); result1.setNextToken("nextToken"); SelectResult result2 = mkSelectResult("testId2"); ArgumentCaptor arg = ArgumentCaptor.forClass(SelectRequest.class); when(sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2); Map query = new LinkedHashMap(); query.put("instanceId", "testId1"); verifyEvents(findEvents(query, new Date(0))); verify(sdbMock, times(2)).select(arg.capture()); SelectRequest req = arg.getValue(); StringBuilder sb = new StringBuilder(); sb.append("select * from `DOMAIN` where region = 'region'"); sb.append(" and instanceId = 'testId1'"); Assert.assertEquals(req.getSelectExpression(), sb.toString() + " and eventTime > '0' order by eventTime desc"); // reset for next test when(sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2); verifyEvents(findEvents(Type.MONKEY, query, new Date(0))); verify(sdbMock, times(4)).select(arg.capture()); req = arg.getValue(); sb.append(" and monkeyType = 'MONKEY|com.netflix.simianarmy.aws.TestSimpleDBRecorder$Type'"); Assert.assertEquals(req.getSelectExpression(), sb.toString() + " and eventTime > '0' order by eventTime desc"); // reset for next test when(sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2); verifyEvents(findEvents(Type.MONKEY, EventTypes.EVENT, query, new Date(0))); verify(sdbMock, times(6)).select(arg.capture()); req = arg.getValue(); sb.append(" and eventType = 'EVENT|com.netflix.simianarmy.aws.TestSimpleDBRecorder$EventTypes'"); Assert.assertEquals(req.getSelectExpression(), sb.toString() + " and eventTime > '0' order by eventTime desc"); // reset for next test when(sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2); verifyEvents(findEvents(Type.MONKEY, EventTypes.EVENT, query, new Date(1330538400000L))); verify(sdbMock, times(8)).select(arg.capture()); req = arg.getValue(); sb.append(" and eventTime > '1330538400000' order by eventTime desc"); Assert.assertEquals(req.getSelectExpression(), sb.toString()); } void verifyEvents(List events) { Assert.assertEquals(events.size(), 2); Assert.assertEquals(events.get(0).id(), "testId1"); Assert.assertEquals(events.get(0).eventTime().getTime(), 1330538400000L); Assert.assertEquals(events.get(0).monkeyType(), Type.MONKEY); Assert.assertEquals(events.get(0).eventType(), EventTypes.EVENT); Assert.assertEquals(events.get(0).field("field1"), "value1"); Assert.assertEquals(events.get(0).field("field2"), "value2"); Assert.assertEquals(events.get(0).fields().size(), 2); Assert.assertEquals(events.get(1).id(), "testId2"); Assert.assertEquals(events.get(1).eventTime().getTime(), 1330538400000L); Assert.assertEquals(events.get(1).monkeyType(), Type.MONKEY); Assert.assertEquals(events.get(1).eventType(), EventTypes.EVENT); Assert.assertEquals(events.get(1).field("field1"), "value1"); Assert.assertEquals(events.get(1).field("field2"), "value2"); Assert.assertEquals(events.get(1).fields().size(), 2); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/conformity/TestASGOwnerEmailTag.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws.conformity; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.TagDescription; import com.google.common.collect.Maps; import com.netflix.simianarmy.aws.conformity.crawler.AWSClusterCrawler; import com.netflix.simianarmy.basic.BasicConfiguration; import com.netflix.simianarmy.basic.conformity.BasicConformityMonkeyContext; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.conformity.Cluster; import junit.framework.Assert; import org.testng.annotations.Test; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TestASGOwnerEmailTag { private static final String ASG1 = "asg1"; private static final String ASG2 = "asg2"; private static final String OWNER_TAG_KEY = "owner"; private static final String OWNER_TAG_VALUE = "tyler@paperstreet.com"; private static final String REGION = "eu-west-1"; @Test public void testForOwnerTag() { Properties properties = new Properties(); BasicConformityMonkeyContext ctx = new BasicConformityMonkeyContext(); List asgList = createASGList(); String[] asgNames = {ASG1, ASG2}; AWSClient awsMock = createMockAWSClient(asgList, asgNames); Map regionToAwsClient = Maps.newHashMap(); regionToAwsClient.put("us-east-1", awsMock); AWSClusterCrawler clusterCrawler = new AWSClusterCrawler(regionToAwsClient, new BasicConfiguration(properties)); List clusters = clusterCrawler.clusters(asgNames); Assert.assertTrue(OWNER_TAG_VALUE.equalsIgnoreCase(clusters.get(0).getOwnerEmail())); Assert.assertNull(clusters.get(1).getOwnerEmail()); } private List createASGList() { List asgList = new LinkedList(); asgList.add(makeASG(ASG1, OWNER_TAG_VALUE)); asgList.add(makeASG(ASG2, null)); return asgList; } private AutoScalingGroup makeASG(String asgName, String ownerEmail) { TagDescription tag = new TagDescription().withKey(OWNER_TAG_KEY).withValue(ownerEmail); AutoScalingGroup asg = new AutoScalingGroup() .withAutoScalingGroupName(asgName) .withTags(tag); return asg; } private AWSClient createMockAWSClient(List asgList, String... asgNames) { AWSClient awsMock = mock(AWSClient.class); when(awsMock.describeAutoScalingGroups(asgNames)).thenReturn(asgList); return awsMock; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/conformity/TestRDSConformityClusterTracker.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck // CHECKSTYLE IGNORE ParameterNumber package com.netflix.simianarmy.aws.conformity; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.mockito.Mockito; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; public class TestRDSConformityClusterTracker extends RDSConformityClusterTracker { public TestRDSConformityClusterTracker() { super(mock(JdbcTemplate.class), "conformitytable"); } @Test public void testInit() { TestRDSConformityClusterTracker recorder = new TestRDSConformityClusterTracker(); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); Mockito.doNothing().when(recorder.getJdbcTemplate()).execute(sqlCap.capture()); recorder.init(); 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) )"); } @SuppressWarnings("unchecked") @Test public void testInsertNewCluster() { // mock the select query that is issued to see if the record already exists ArrayList clusters = new ArrayList<>(); TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker(); when(tracker.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(clusters); Cluster cluster1 = new Cluster("clustername1", "us-west-1"); cluster1.setUpdateTime(new Date()); ArrayList list = new ArrayList<>(); list.add("one"); list.add("two"); cluster1.updateConformity(new Conformity("rule1",list)); list.add("three"); cluster1.updateConformity(new Conformity("rule2",list)); ArgumentCaptor objCap = ArgumentCaptor.forClass(Object.class); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); when(tracker.getJdbcTemplate().update(sqlCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture())).thenReturn(1); tracker.addOrUpdate(cluster1); List args = objCap.getAllValues(); Assert.assertEquals(args.size(), 9); Assert.assertEquals(args.get(0).toString(), "clustername1"); Assert.assertEquals(args.get(1).toString(), "us-west-1"); Assert.assertEquals(args.get(7).toString(), "{\"rule1\":\"one,two\",\"rule2\":\"one,two,three\"}"); Assert.assertEquals(args.get(8).toString(), "rule1,rule2"); } @SuppressWarnings("unchecked") @Test public void testUpdateCluster() { Cluster cluster1 = new Cluster("clustername1", "us-west-1"); Date date = new Date(); cluster1.setUpdateTime(date); // mock the select query that is issued to see if the record already exists ArrayList clusters = new ArrayList<>(); clusters.add(cluster1); TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker(); when(tracker.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(clusters); cluster1.setOwnerEmail("newemail@test.com"); ArgumentCaptor objCap = ArgumentCaptor.forClass(Object.class); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); when(tracker.getJdbcTemplate().update(sqlCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture())).thenReturn(1); tracker.addOrUpdate(cluster1); List args = objCap.getAllValues(); Assert.assertEquals(sqlCap.getValue(), "update conformitytable set ownerEmail=?,isConforming=?,isOptedOut=?,updateTimestamp=?,excludedRules=?,conformities=?,conformityRules=? where cluster=? and region=?"); Assert.assertEquals(args.size(), 9); Assert.assertEquals(args.get(0).toString(), "newemail@test.com"); Assert.assertEquals(args.get(1).toString(), "false"); Assert.assertEquals(args.get(2).toString(), "false"); Assert.assertEquals(args.get(3).toString(), date.getTime() + ""); Assert.assertEquals(args.get(7).toString(), "clustername1"); Assert.assertEquals(args.get(8).toString(), "us-west-1"); } @SuppressWarnings("unchecked") @Test public void testGetCluster() { Cluster cluster1 = new Cluster("clustername1", "us-west-1"); ArrayList clusters = new ArrayList<>(); clusters.add(cluster1); TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker(); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); when(tracker.getJdbcTemplate().query(sqlCap.capture(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(clusters); Cluster result = tracker.getCluster("clustername1", "us-west-1"); Assert.assertNotNull(result); Assert.assertEquals(sqlCap.getValue(), "select * from conformitytable where cluster = ? and region = ?"); } @SuppressWarnings("unchecked") @Test public void testGetClusters() { Cluster cluster1 = new Cluster("clustername1", "us-west-1"); Cluster cluster2 = new Cluster("clustername1", "us-west-2"); Cluster cluster3 = new Cluster("clustername1", "us-east-1"); ArrayList clusters = new ArrayList<>(); clusters.add(cluster1); clusters.add(cluster2); clusters.add(cluster3); TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker(); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); when(tracker.getJdbcTemplate().query(sqlCap.capture(), Matchers.any(RowMapper.class))).thenReturn(clusters); List results = tracker.getAllClusters("us-west-1", "us-west-2", "us-east-1"); Assert.assertEquals(results.size(), 3); Assert.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')"); } @SuppressWarnings("unchecked") @Test public void testGetClusterNotFound() { ArrayList clusters = new ArrayList<>(); TestRDSConformityClusterTracker tracker = new TestRDSConformityClusterTracker(); when(tracker.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(clusters); Cluster cluster = tracker.getCluster("clustername", "us-west-1"); Assert.assertNull(cluster); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/conformity/rule/TestInstanceInVPC.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws.conformity.rule; import com.amazonaws.services.ec2.model.Instance; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.netflix.simianarmy.conformity.AutoScalingGroup; import com.netflix.simianarmy.conformity.Cluster; import com.netflix.simianarmy.conformity.Conformity; import junit.framework.Assert; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.List; import java.util.Set; import static org.mockito.Mockito.doReturn; public class TestInstanceInVPC { private static final String VPC_INSTANCE_ID = "abc-123"; private static final String INSTANCE_ID = "zxy-098"; private static final String REGION = "eu-west-1"; @Spy private InstanceInVPC instanceInVPC = new InstanceInVPC(); @BeforeMethod public void setUp() throws Exception { MockitoAnnotations.initMocks(this); List instanceList = Lists.newArrayList(); Instance instance = new Instance().withInstanceId(VPC_INSTANCE_ID).withVpcId("12345"); instanceList.add(instance); doReturn(instanceList).when(instanceInVPC).getAWSInstances(REGION, VPC_INSTANCE_ID); List instanceList2 = Lists.newArrayList(); Instance instance2 = new Instance().withInstanceId(INSTANCE_ID); instanceList2.add(instance2); doReturn(instanceList2).when(instanceInVPC).getAWSInstances(REGION, INSTANCE_ID); } @Test public void testCheckSoloInstances() throws Exception { Set list = Sets.newHashSet(); list.add(VPC_INSTANCE_ID); list.add(INSTANCE_ID); Cluster cluster = new Cluster("SoloInstances", REGION, list); Conformity result = instanceInVPC.check(cluster); Assert.assertNotNull(result); Assert.assertEquals(result.getRuleId(), instanceInVPC.getName()); Assert.assertEquals(result.getFailedComponents().size(), 1); Assert.assertEquals(result.getFailedComponents().iterator().next(), INSTANCE_ID); } @Test public void testAsgInstances() throws Exception { AutoScalingGroup autoScalingGroup = new AutoScalingGroup("Conforming", VPC_INSTANCE_ID); Cluster conformingCluster = new Cluster("Conforming", REGION, autoScalingGroup); Conformity result = instanceInVPC.check(conformingCluster); Assert.assertNotNull(result); Assert.assertEquals(result.getRuleId(), instanceInVPC.getName()); Assert.assertEquals(result.getFailedComponents().size(), 0); autoScalingGroup = new AutoScalingGroup("NonConforming", INSTANCE_ID); Cluster nonConformingCluster = new Cluster("NonConforming", REGION, autoScalingGroup); result = instanceInVPC.check(nonConformingCluster); Assert.assertNotNull(result); Assert.assertEquals(result.getRuleId(), instanceInVPC.getName()); Assert.assertEquals(result.getFailedComponents().size(), 1); Assert.assertEquals(result.getFailedComponents().iterator().next(), INSTANCE_ID); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/TestAWSResource.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor; import java.lang.reflect.Field; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; public class TestAWSResource { /** Make sure the getFieldToValue returns the right field and values. * @throws Exception **/ @Test public void testFieldToValueMapWithoutNullForInstance() throws Exception { Date now = new Date(); Resource resource = getTestingResource(now); Map resourceFieldValueMap = resource.getFieldToValueMap(); verifyMapsAreEqual(resourceFieldValueMap, getTestingFieldValueMap(now, getTestingFields())); } /** * When all fields are null, the map returned is empty. */ @Test public void testFieldToValueMapWithNull() { Resource resource = new AWSResource(); Map resourceFieldValueMap = resource.getFieldToValueMap(); // The only value in the map is the boolean of opt out Assert.assertEquals(resourceFieldValueMap.size(), 1); } @Test public void testParseFieldToValueMap() throws Exception { Date now = new Date(); Map map = getTestingFieldValueMap(now, getTestingFields()); AWSResource resource = AWSResource.parseFieldtoValueMap(map); Map resourceFieldValueMap = resource.getFieldToValueMap(); verifyMapsAreEqual(resourceFieldValueMap, map); } @Test public void testClone() { Date now = new Date(); Resource resource = getTestingResource(now); Resource clone = resource.cloneResource(); verifyMapsAreEqual(clone.getFieldToValueMap(), resource.getFieldToValueMap()); verifyTagsAreEqual(clone, resource); } private void verifyMapsAreEqual(Map map1, Map map2) { Assert.assertFalse(map1 == null ^ map2 == null); Assert.assertEquals(map1.size(), map2.size()); for (Map.Entry entry : map1.entrySet()) { Assert.assertEquals(entry.getValue(), map2.get(entry.getKey())); } } private void verifyTagsAreEqual(Resource r1, Resource r2) { Collection keys1 = r1.getAllTagKeys(); Collection keys2 = r2.getAllTagKeys(); Assert.assertEquals(keys1.size(), keys2.size()); for (String key : keys1) { Assert.assertEquals(r1.getTag(key), r2.getTag(key)); } } private Map getTestingFieldValueMap(Date defaultDate, Map additionalFields) throws Exception { Field[] fields = AWSResource.class.getFields(); Map fieldToValue = new HashMap(); String dateString = AWSResource.DATE_FORMATTER.print(defaultDate.getTime()); for (Field field : fields) { if (field.getName().startsWith("FIELD_")) { String value; String key = (String) (field.get(null)); if (field.getName().endsWith("TIME")) { value = dateString; } else if (field.getName().equals("FIELD_STATE")) { value = "MARKED"; } else if (field.getName().equals("FIELD_RESOURCE_TYPE")) { value = "INSTANCE"; } else if (field.getName().equals("FIELD_OPT_OUT_OF_JANITOR")) { value = "false"; } else { value = (String) (field.get(null)); } fieldToValue.put(key, value); } } if (additionalFields != null) { fieldToValue.putAll(additionalFields); } return fieldToValue; } private Resource getTestingResource(Date now) { String id = "resourceId"; Resource resource = new AWSResource().withId(id).withRegion("region").withResourceType(AWSResourceType.INSTANCE) .withState(Resource.CleanupState.MARKED).withDescription("description") .withExpectedTerminationTime(now).withActualTerminationTime(now) .withLaunchTime(now).withMarkTime(now).withNnotificationTime(now).withOwnerEmail("ownerEmail") .withTerminationReason("terminationReason").withOptOutOfJanitor(false); ((AWSResource) resource).setAWSResourceState("awsResourceState"); for (Map.Entry field : getTestingFields().entrySet()) { resource.setAdditionalField(field.getKey(), field.getValue()); } for (int i = 1; i < 10; i++) { resource.setTag("tagKey_" + i, "tagValue_" + i); } return resource; } private Map getTestingFields() { Map map = new HashMap(); for (int i = 0; i < 10; i++) { map.put("name" + i, "value" + i); } return map; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/TestRDSJanitorResourceTracker.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck // CHECKSTYLE IGNORE ParameterNumber package com.netflix.simianarmy.aws.janitor; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import org.joda.time.DateTime; import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.mockito.Mockito; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.testng.Assert; import org.testng.annotations.Test; import java.util.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TestRDSJanitorResourceTracker extends RDSJanitorResourceTracker { public TestRDSJanitorResourceTracker() { super(mock(JdbcTemplate.class), "janitortable"); } @Test public void testInit() { TestRDSJanitorResourceTracker recorder = new TestRDSJanitorResourceTracker(); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); Mockito.doNothing().when(recorder.getJdbcTemplate()).execute(sqlCap.capture()); recorder.init(); 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) )"); } @SuppressWarnings("unchecked") @Test public void testInsertNewResource() { // mock the select query that is issued to see if the record already exists ArrayList resources = new ArrayList<>(); TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker(); when(tracker.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(resources); String id = "i-12345678901234567"; AWSResourceType resourceType = AWSResourceType.INSTANCE; Resource.CleanupState state = Resource.CleanupState.MARKED; String description = "This is a test resource."; String ownerEmail = "owner@test.com"; String region = "us-east-1"; String terminationReason = "This is a test termination reason."; DateTime now = DateTime.now(); Date expectedTerminationTime = new Date(now.plusDays(10).getMillis()); Date markTime = new Date(now.getMillis()); String fieldName = "fieldName123"; String fieldValue = "fieldValue456"; Resource resource = new AWSResource().withId(id).withResourceType(resourceType) .withDescription(description).withOwnerEmail(ownerEmail).withRegion(region) .withState(state).withTerminationReason(terminationReason) .withExpectedTerminationTime(expectedTerminationTime) .withMarkTime(markTime).withOptOutOfJanitor(false) .setAdditionalField(fieldName, fieldValue); ArgumentCaptor objCap = ArgumentCaptor.forClass(Object.class); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); when(tracker.getJdbcTemplate().update(sqlCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture())).thenReturn(1); tracker.addOrUpdate(resource); List args = objCap.getAllValues(); Assert.assertEquals(sqlCap.getValue(), "insert into janitortable (resourceId,resourceType,region,ownerEmail,description,state,terminationReason,expectedTerminationTime,actualTerminationTime,notificationTime,launchTime,markTime,optOutOfJanitor,additionalFields) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); Assert.assertEquals(args.size(), 14); Assert.assertEquals(args.get(0).toString(), id); Assert.assertEquals(args.get(1).toString(), AWSResourceType.INSTANCE.toString()); Assert.assertEquals(args.get(2).toString(), region); Assert.assertEquals(args.get(3).toString(), ownerEmail); Assert.assertEquals(args.get(4).toString(), description); Assert.assertEquals(args.get(5).toString(), state.toString()); Assert.assertEquals(args.get(6).toString(), terminationReason); Assert.assertEquals(args.get(7).toString(), expectedTerminationTime.getTime() + ""); Assert.assertEquals(args.get(8).toString(), "0"); Assert.assertEquals(args.get(9).toString(), "0"); Assert.assertEquals(args.get(10).toString(), "0"); Assert.assertEquals(args.get(11).toString(), markTime.getTime() + ""); Assert.assertEquals(args.get(12).toString(), "false"); Assert.assertEquals(args.get(13).toString(), "{\"fieldName123\":\"fieldValue456\"}"); } @SuppressWarnings("unchecked") @Test public void testUpdateResource() { String id = "i-12345678901234567"; AWSResourceType resourceType = AWSResourceType.INSTANCE; Resource.CleanupState state = Resource.CleanupState.MARKED; String description = "This is a test resource."; String ownerEmail = "owner@test.com"; String region = "us-east-1"; String terminationReason = "This is a test termination reason."; DateTime now = DateTime.now(); Date expectedTerminationTime = new Date(now.plusDays(10).getMillis()); Date markTime = new Date(now.getMillis()); String fieldName = "fieldName123"; String fieldValue = "fieldValue456"; Resource resource = new AWSResource().withId(id).withResourceType(resourceType) .withDescription(description).withOwnerEmail(ownerEmail).withRegion(region) .withState(state).withTerminationReason(terminationReason) .withExpectedTerminationTime(expectedTerminationTime) .withMarkTime(markTime).withOptOutOfJanitor(false) .setAdditionalField(fieldName, fieldValue); // mock the select query that is issued to see if the record already exists ArrayList resources = new ArrayList<>(); resources.add(resource); TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker(); when(tracker.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(resources); // update the ownerEmail ownerEmail = "owner2@test.com"; Resource newResource = new AWSResource().withId(id).withResourceType(resourceType) .withDescription(description).withOwnerEmail(ownerEmail).withRegion(region) .withState(state).withTerminationReason(terminationReason) .withExpectedTerminationTime(expectedTerminationTime) .withMarkTime(markTime).withOptOutOfJanitor(false) .setAdditionalField(fieldName, fieldValue); ArgumentCaptor objCap = ArgumentCaptor.forClass(Object.class); ArgumentCaptor sqlCap = ArgumentCaptor.forClass(String.class); when(tracker.getJdbcTemplate().update(sqlCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture(), objCap.capture())).thenReturn(1); tracker.addOrUpdate(newResource); List args = objCap.getAllValues(); Assert.assertEquals(sqlCap.getValue(), "update janitortable set resourceType=?,region=?,ownerEmail=?,description=?,state=?,terminationReason=?,expectedTerminationTime=?,actualTerminationTime=?,notificationTime=?,launchTime=?,markTime=?,optOutOfJanitor=?,additionalFields=? where resourceId=? and region=?"); Assert.assertEquals(args.size(), 15); Assert.assertEquals(args.get(0).toString(), AWSResourceType.INSTANCE.toString()); Assert.assertEquals(args.get(1).toString(), region); Assert.assertEquals(args.get(2).toString(), ownerEmail); Assert.assertEquals(args.get(3).toString(), description); Assert.assertEquals(args.get(4).toString(), state.toString()); Assert.assertEquals(args.get(5).toString(), terminationReason); Assert.assertEquals(args.get(6).toString(), expectedTerminationTime.getTime() + ""); Assert.assertEquals(args.get(7).toString(), "0"); Assert.assertEquals(args.get(8).toString(), "0"); Assert.assertEquals(args.get(9).toString(), "0"); Assert.assertEquals(args.get(10).toString(), markTime.getTime() + ""); Assert.assertEquals(args.get(11).toString(), "false"); Assert.assertEquals(args.get(12).toString(), "{\"fieldName123\":\"fieldValue456\"}"); Assert.assertEquals(args.get(13).toString(), id); Assert.assertEquals(args.get(14).toString(), region); } @SuppressWarnings("unchecked") @Test public void testGetResource() { String id1 = "id-1"; AWSResourceType resourceType = AWSResourceType.INSTANCE; Resource.CleanupState state = Resource.CleanupState.MARKED; String description = "This is a test resource."; String ownerEmail = "owner@test.com"; String region = "us-east-1"; String terminationReason = "This is a test termination reason."; DateTime now = DateTime.now(); Date expectedTerminationTime = new Date(now.plusDays(10).getMillis()); Date markTime = new Date(now.getMillis()); String fieldName = "fieldName123"; String fieldValue = "fieldValue456"; AWSResource result1 = mkResource(id1, resourceType, state, description, ownerEmail, region, terminationReason, expectedTerminationTime, markTime, false, fieldName, fieldValue); ArrayList resources = new ArrayList<>(); resources.add(result1); TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker(); when(tracker.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(resources); Resource resource = tracker.getResource(id1); ArrayList returnResources = new ArrayList<>(); returnResources.add(resource); verifyResources(returnResources, id1, null, resourceType, state, description, ownerEmail, region, terminationReason, expectedTerminationTime, markTime, fieldName, fieldValue); } @SuppressWarnings("unchecked") @Test public void testGetResourceNotFound() { ArrayList resources = new ArrayList<>(); TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker(); when(tracker.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(resources); Resource resource = tracker.getResource("id-2"); Assert.assertNull(resource); } @SuppressWarnings("unchecked") @Test public void testGetResources() { String id1 = "id-1"; String id2 = "id-2"; AWSResourceType resourceType = AWSResourceType.INSTANCE; Resource.CleanupState state = Resource.CleanupState.MARKED; String description = "This is a test resource."; String ownerEmail = "owner@test.com"; String region = "us-east-1"; String terminationReason = "This is a test termination reason."; DateTime now = DateTime.now(); Date expectedTerminationTime = new Date(now.plusDays(10).getMillis()); Date markTime = new Date(now.getMillis()); String fieldName = "fieldName123"; String fieldValue = "fieldValue456"; AWSResource result1 = mkResource(id1, resourceType, state, description, ownerEmail, region, terminationReason, expectedTerminationTime, markTime, false, fieldName, fieldValue); AWSResource result2 = mkResource(id2, resourceType, state, description, ownerEmail, region, terminationReason, expectedTerminationTime, markTime, true, fieldName, fieldValue); ArrayList resources = new ArrayList<>(); resources.add(result1); resources.add(result2); TestRDSJanitorResourceTracker tracker = new TestRDSJanitorResourceTracker(); when(tracker.getJdbcTemplate().query(Matchers.anyString(), Matchers.any(Object[].class), Matchers.any(RowMapper.class))).thenReturn(resources); verifyResources(tracker.getResources(resourceType, state, region), id1, id2, resourceType, state, description, ownerEmail, region, terminationReason, expectedTerminationTime, markTime, fieldName, fieldValue); } private void verifyResources(List resources, String id1, String id2, AWSResourceType resourceType, Resource.CleanupState state, String description, String ownerEmail, String region, String terminationReason, Date expectedTerminationTime, Date markTime, String fieldName, String fieldValue) { if (id2 == null) { Assert.assertEquals(resources.size(), 1); } else { Assert.assertEquals(resources.size(), 2); } Assert.assertEquals(resources.get(0).getId(), id1); Assert.assertEquals(resources.get(0).getResourceType(), resourceType); Assert.assertEquals(resources.get(0).getState(), state); Assert.assertEquals(resources.get(0).getDescription(), description); Assert.assertEquals(resources.get(0).getOwnerEmail(), ownerEmail); Assert.assertEquals(resources.get(0).getRegion(), region); Assert.assertEquals(resources.get(0).getTerminationReason(), terminationReason); Assert.assertEquals( AWSResource.DATE_FORMATTER.print(resources.get(0).getExpectedTerminationTime().getTime()), AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime())); Assert.assertEquals( AWSResource.DATE_FORMATTER.print(resources.get(0).getMarkTime().getTime()), AWSResource.DATE_FORMATTER.print(markTime.getTime())); Assert.assertEquals(resources.get(0).getAdditionalField(fieldName), fieldValue); Assert.assertEquals(resources.get(0).isOptOutOfJanitor(), false); if (id2 != null) { Assert.assertEquals(resources.get(1).getId(), id2); Assert.assertEquals(resources.get(1).getResourceType(), resourceType); Assert.assertEquals(resources.get(1).getState(), state); Assert.assertEquals(resources.get(1).getDescription(), description); Assert.assertEquals(resources.get(1).getOwnerEmail(), ownerEmail); Assert.assertEquals(resources.get(1).getRegion(), region); Assert.assertEquals(resources.get(1).getTerminationReason(), terminationReason); Assert.assertEquals( AWSResource.DATE_FORMATTER.print(resources.get(1).getExpectedTerminationTime().getTime()), AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime())); Assert.assertEquals( AWSResource.DATE_FORMATTER.print(resources.get(1).getMarkTime().getTime()), AWSResource.DATE_FORMATTER.print(markTime.getTime())); Assert.assertEquals(resources.get(1).isOptOutOfJanitor(), true); Assert.assertEquals(resources.get(1).getAdditionalField(fieldName), fieldValue); } } private AWSResource mkResource(String id, AWSResourceType resourceType, Resource.CleanupState state, String description, String ownerEmail, String region, String terminationReason, Date expectedTerminationTime, Date markTime, boolean optOut, String fieldName, String fieldValue) { Map attrs = new HashMap<>(); attrs.put(AWSResource.FIELD_RESOURCE_ID, id); attrs.put(AWSResource.FIELD_RESOURCE_TYPE, resourceType.name()); attrs.put(AWSResource.FIELD_DESCRIPTION, description); attrs.put(AWSResource.FIELD_REGION, region); attrs.put(AWSResource.FIELD_STATE, state.name()); attrs.put(AWSResource.FIELD_OWNER_EMAIL, ownerEmail); attrs.put(AWSResource.FIELD_TERMINATION_REASON, terminationReason); attrs.put(AWSResource.FIELD_EXPECTED_TERMINATION_TIME, AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime())); attrs.put(AWSResource.FIELD_MARK_TIME, AWSResource.DATE_FORMATTER.print(markTime.getTime())); attrs.put(AWSResource.FIELD_OPT_OUT_OF_JANITOR, String.valueOf(optOut)); attrs.put(fieldName, fieldValue); return AWSResource.parseFieldtoValueMap(attrs); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/TestSimpleDBJanitorResourceTracker.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck // CHECKSTYLE IGNORE ParameterNumber package com.netflix.simianarmy.aws.janitor; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.joda.time.DateTime; import org.mockito.ArgumentCaptor; import org.testng.Assert; import org.testng.annotations.Test; import com.amazonaws.services.simpledb.AmazonSimpleDB; import com.amazonaws.services.simpledb.model.Attribute; import com.amazonaws.services.simpledb.model.Item; import com.amazonaws.services.simpledb.model.PutAttributesRequest; import com.amazonaws.services.simpledb.model.ReplaceableAttribute; import com.amazonaws.services.simpledb.model.SelectRequest; import com.amazonaws.services.simpledb.model.SelectResult; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; public class TestSimpleDBJanitorResourceTracker extends SimpleDBJanitorResourceTracker { private static AWSClient makeMockAWSClient() { AmazonSimpleDB sdbMock = mock(AmazonSimpleDB.class); AWSClient awsClient = mock(AWSClient.class); when(awsClient.sdbClient()).thenReturn(sdbMock); return awsClient; } public TestSimpleDBJanitorResourceTracker() { super(makeMockAWSClient(), "DOMAIN"); sdbMock = super.getSimpleDBClient(); } private final AmazonSimpleDB sdbMock; @Test public void testAddResource() { String id = "i-12345678901234567"; AWSResourceType resourceType = AWSResourceType.INSTANCE; Resource.CleanupState state = Resource.CleanupState.MARKED; String description = "This is a test resource."; String ownerEmail = "owner@test.com"; String region = "us-east-1"; String terminationReason = "This is a test termination reason."; DateTime now = DateTime.now(); Date expectedTerminationTime = new Date(now.plusDays(10).getMillis()); Date markTime = new Date(now.getMillis()); String fieldName = "fieldName123"; String fieldValue = "fieldValue456"; Resource resource = new AWSResource().withId(id).withResourceType(resourceType) .withDescription(description).withOwnerEmail(ownerEmail).withRegion(region) .withState(state).withTerminationReason(terminationReason) .withExpectedTerminationTime(expectedTerminationTime) .withMarkTime(markTime).withOptOutOfJanitor(false) .setAdditionalField(fieldName, fieldValue); ArgumentCaptor arg = ArgumentCaptor.forClass(PutAttributesRequest.class); TestSimpleDBJanitorResourceTracker tracker = new TestSimpleDBJanitorResourceTracker(); tracker.addOrUpdate(resource); verify(tracker.sdbMock).putAttributes(arg.capture()); PutAttributesRequest req = arg.getValue(); Assert.assertEquals(req.getDomainName(), "DOMAIN"); Assert.assertEquals(req.getItemName(), getSimpleDBItemName(resource)); Map map = new HashMap(); for (ReplaceableAttribute attr : req.getAttributes()) { map.put(attr.getName(), attr.getValue()); } Assert.assertEquals(map.remove(AWSResource.FIELD_RESOURCE_ID), id); Assert.assertEquals(map.remove(AWSResource.FIELD_DESCRIPTION), description); Assert.assertEquals(map.remove(AWSResource.FIELD_EXPECTED_TERMINATION_TIME), AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime())); Assert.assertEquals(map.remove(AWSResource.FIELD_MARK_TIME), AWSResource.DATE_FORMATTER.print(markTime.getTime())); Assert.assertEquals(map.remove(AWSResource.FIELD_REGION), region); Assert.assertEquals(map.remove(AWSResource.FIELD_OWNER_EMAIL), ownerEmail); Assert.assertEquals(map.remove(AWSResource.FIELD_RESOURCE_TYPE), resourceType.name()); Assert.assertEquals(map.remove(AWSResource.FIELD_STATE), state.name()); Assert.assertEquals(map.remove(AWSResource.FIELD_TERMINATION_REASON), terminationReason); Assert.assertEquals(map.remove(AWSResource.FIELD_OPT_OUT_OF_JANITOR), "false"); Assert.assertEquals(map.remove(fieldName), fieldValue); Assert.assertEquals(map.size(), 0); } @Test public void testGetResources() { String id1 = "id-1"; String id2 = "id-2"; AWSResourceType resourceType = AWSResourceType.INSTANCE; Resource.CleanupState state = Resource.CleanupState.MARKED; String description = "This is a test resource."; String ownerEmail = "owner@test.com"; String region = "us-east-1"; String terminationReason = "This is a test termination reason."; DateTime now = DateTime.now(); Date expectedTerminationTime = new Date(now.plusDays(10).getMillis()); Date markTime = new Date(now.getMillis()); String fieldName = "fieldName123"; String fieldValue = "fieldValue456"; SelectResult result1 = mkSelectResult(id1, resourceType, state, description, ownerEmail, region, terminationReason, expectedTerminationTime, markTime, false, fieldName, fieldValue); result1.setNextToken("nextToken"); SelectResult result2 = mkSelectResult(id2, resourceType, state, description, ownerEmail, region, terminationReason, expectedTerminationTime, markTime, true, fieldName, fieldValue); ArgumentCaptor arg = ArgumentCaptor.forClass(SelectRequest.class); TestSimpleDBJanitorResourceTracker tracker = new TestSimpleDBJanitorResourceTracker(); when(tracker.sdbMock.select(any(SelectRequest.class))).thenReturn(result1).thenReturn(result2); verifyResources(tracker.getResources(resourceType, state, region), id1, id2, resourceType, state, description, ownerEmail, region, terminationReason, expectedTerminationTime, markTime, fieldName, fieldValue); verify(tracker.sdbMock, times(2)).select(arg.capture()); } private void verifyResources(List resources, String id1, String id2, AWSResourceType resourceType, Resource.CleanupState state, String description, String ownerEmail, String region, String terminationReason, Date expectedTerminationTime, Date markTime, String fieldName, String fieldValue) { Assert.assertEquals(resources.size(), 2); Assert.assertEquals(resources.get(0).getId(), id1); Assert.assertEquals(resources.get(0).getResourceType(), resourceType); Assert.assertEquals(resources.get(0).getState(), state); Assert.assertEquals(resources.get(0).getDescription(), description); Assert.assertEquals(resources.get(0).getOwnerEmail(), ownerEmail); Assert.assertEquals(resources.get(0).getRegion(), region); Assert.assertEquals(resources.get(0).getTerminationReason(), terminationReason); Assert.assertEquals( AWSResource.DATE_FORMATTER.print(resources.get(0).getExpectedTerminationTime().getTime()), AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime())); Assert.assertEquals( AWSResource.DATE_FORMATTER.print(resources.get(0).getMarkTime().getTime()), AWSResource.DATE_FORMATTER.print(markTime.getTime())); Assert.assertEquals(resources.get(0).getAdditionalField(fieldName), fieldValue); Assert.assertEquals(resources.get(0).isOptOutOfJanitor(), false); Assert.assertEquals(resources.get(1).getId(), id2); Assert.assertEquals(resources.get(1).getResourceType(), resourceType); Assert.assertEquals(resources.get(1).getState(), state); Assert.assertEquals(resources.get(1).getDescription(), description); Assert.assertEquals(resources.get(1).getOwnerEmail(), ownerEmail); Assert.assertEquals(resources.get(1).getRegion(), region); Assert.assertEquals(resources.get(1).getTerminationReason(), terminationReason); Assert.assertEquals( AWSResource.DATE_FORMATTER.print(resources.get(1).getExpectedTerminationTime().getTime()), AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime())); Assert.assertEquals( AWSResource.DATE_FORMATTER.print(resources.get(1).getMarkTime().getTime()), AWSResource.DATE_FORMATTER.print(markTime.getTime())); Assert.assertEquals(resources.get(1).isOptOutOfJanitor(), true); Assert.assertEquals(resources.get(1).getAdditionalField(fieldName), fieldValue); } private SelectResult mkSelectResult(String id, AWSResourceType resourceType, Resource.CleanupState state, String description, String ownerEmail, String region, String terminationReason, Date expectedTerminationTime, Date markTime, boolean optOut, String fieldName, String fieldValue) { Item item = new Item(); List attrs = new LinkedList(); attrs.add(new Attribute(AWSResource.FIELD_RESOURCE_ID, id)); attrs.add(new Attribute(AWSResource.FIELD_RESOURCE_TYPE, resourceType.name())); attrs.add(new Attribute(AWSResource.FIELD_DESCRIPTION, description)); attrs.add(new Attribute(AWSResource.FIELD_REGION, region)); attrs.add(new Attribute(AWSResource.FIELD_STATE, state.name())); attrs.add(new Attribute(AWSResource.FIELD_OWNER_EMAIL, ownerEmail)); attrs.add(new Attribute(AWSResource.FIELD_TERMINATION_REASON, terminationReason)); attrs.add(new Attribute(AWSResource.FIELD_EXPECTED_TERMINATION_TIME, AWSResource.DATE_FORMATTER.print(expectedTerminationTime.getTime()))); attrs.add(new Attribute(AWSResource.FIELD_MARK_TIME, AWSResource.DATE_FORMATTER.print(markTime.getTime()))); attrs.add(new Attribute(AWSResource.FIELD_OPT_OUT_OF_JANITOR, String.valueOf(optOut))); attrs.add(new Attribute(fieldName, fieldValue)); item.setAttributes(attrs); item.setName(String.format("%s-%s-%s", resourceType.name(), id, region)); SelectResult result = new SelectResult(); result.setItems(Arrays.asList(item)); return result; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestASGJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws.janitor.crawler; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import org.testng.Assert; import org.testng.annotations.Test; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.SuspendedProcess; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; public class TestASGJanitorCrawler { @Test public void testResourceTypes() { ASGJanitorCrawler crawler = new ASGJanitorCrawler(createMockAWSClient(createASGList())); EnumSet types = crawler.resourceTypes(); Assert.assertEquals(types.size(), 1); Assert.assertEquals(types.iterator().next().name(), "ASG"); } @Test public void testInstancesWithNullNames() { List asgList = createASGList(); AWSClient awsMock = createMockAWSClient(asgList); ASGJanitorCrawler crawler = new ASGJanitorCrawler(awsMock); List resources = crawler.resources(); verifyASGList(resources, asgList); } @Test public void testInstancesWithNames() { List asgList = createASGList(); String[] asgNames = {"asg1", "asg2"}; AWSClient awsMock = createMockAWSClient(asgList, asgNames); ASGJanitorCrawler crawler = new ASGJanitorCrawler(awsMock); List resources = crawler.resources(asgNames); verifyASGList(resources, asgList); } @Test public void testInstancesWithResourceType() { List asgList = createASGList(); AWSClient awsMock = createMockAWSClient(asgList); ASGJanitorCrawler crawler = new ASGJanitorCrawler(awsMock); for (AWSResourceType resourceType : AWSResourceType.values()) { List resources = crawler.resources(resourceType); if (resourceType == AWSResourceType.ASG) { verifyASGList(resources, asgList); } else { Assert.assertTrue(resources.isEmpty()); } } } private void verifyASGList(List resources, List asgList) { Assert.assertEquals(resources.size(), asgList.size()); for (int i = 0; i < resources.size(); i++) { AutoScalingGroup asg = asgList.get(i); verifyASG(resources.get(i), asg.getAutoScalingGroupName()); } } private void verifyASG(Resource asg, String asgName) { Assert.assertEquals(asg.getResourceType(), AWSResourceType.ASG); Assert.assertEquals(asg.getId(), asgName); Assert.assertEquals(asg.getRegion(), "us-east-1"); Assert.assertEquals(asg.getAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME), "2012-12-03T23:00:03"); } private AWSClient createMockAWSClient(List asgList, String... asgNames) { AWSClient awsMock = mock(AWSClient.class); when(awsMock.describeAutoScalingGroups(asgNames)).thenReturn(asgList); when(awsMock.region()).thenReturn("us-east-1"); return awsMock; } private List createASGList() { List asgList = new LinkedList(); asgList.add(mkASG("asg1")); asgList.add(mkASG("asg2")); return asgList; } private AutoScalingGroup mkASG(String asgName) { AutoScalingGroup asg = new AutoScalingGroup().withAutoScalingGroupName(asgName); // set the suspended processes List sps = new ArrayList(); sps.add(new SuspendedProcess().withProcessName("Launch") .withSuspensionReason("User suspended at 2012-12-02T23:00:03")); sps.add(new SuspendedProcess().withProcessName("AddToLoadBalancer") .withSuspensionReason("User suspended at 2012-12-03T23:00:03")); asg.setSuspendedProcesses(sps); return asg; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestEBSSnapshotJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ //CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws.janitor.crawler; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Date; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import org.testng.Assert; import org.testng.annotations.Test; import com.amazonaws.services.ec2.model.Snapshot; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; public class TestEBSSnapshotJanitorCrawler { @Test public void testResourceTypes() { Date startTime = new Date(); List snapshotList = createSnapshotList(startTime); EBSSnapshotJanitorCrawler crawler = new EBSSnapshotJanitorCrawler(createMockAWSClient(snapshotList)); EnumSet types = crawler.resourceTypes(); Assert.assertEquals(types.size(), 1); Assert.assertEquals(types.iterator().next().name(), "EBS_SNAPSHOT"); } @Test public void testSnapshotsWithNullIds() { Date startTime = new Date(); List snapshotList = createSnapshotList(startTime); EBSSnapshotJanitorCrawler crawler = new EBSSnapshotJanitorCrawler(createMockAWSClient(snapshotList)); List resources = crawler.resources(); verifySnapshotList(resources, snapshotList, startTime); } @Test public void testSnapshotsWithIds() { Date startTime = new Date(); List snapshotList = createSnapshotList(startTime); String[] ids = {"snap-12345678901234567", "snap-12345678901234567"}; EBSSnapshotJanitorCrawler crawler = new EBSSnapshotJanitorCrawler(createMockAWSClient(snapshotList, ids)); List resources = crawler.resources(ids); verifySnapshotList(resources, snapshotList, startTime); } @Test public void testSnapshotsWithResourceType() { Date startTime = new Date(); List snapshotList = createSnapshotList(startTime); EBSSnapshotJanitorCrawler crawler = new EBSSnapshotJanitorCrawler(createMockAWSClient(snapshotList)); for (AWSResourceType resourceType : AWSResourceType.values()) { List resources = crawler.resources(resourceType); if (resourceType == AWSResourceType.EBS_SNAPSHOT) { verifySnapshotList(resources, snapshotList, startTime); } else { Assert.assertTrue(resources.isEmpty()); } } } private void verifySnapshotList(List resources, List snapshotList, Date startTime) { Assert.assertEquals(resources.size(), snapshotList.size()); for (int i = 0; i < resources.size(); i++) { Snapshot snapshot = snapshotList.get(i); verifySnapshot(resources.get(i), snapshot.getSnapshotId(), startTime); } } private void verifySnapshot(Resource snapshot, String snapshotId, Date startTime) { Assert.assertEquals(snapshot.getResourceType(), AWSResourceType.EBS_SNAPSHOT); Assert.assertEquals(snapshot.getId(), snapshotId); Assert.assertEquals(snapshot.getRegion(), "us-east-1"); Assert.assertEquals(((AWSResource) snapshot).getAWSResourceState(), "completed"); Assert.assertEquals(snapshot.getLaunchTime(), startTime); } private AWSClient createMockAWSClient(List snapshotList, String... ids) { AWSClient awsMock = mock(AWSClient.class); when(awsMock.describeSnapshots(ids)).thenReturn(snapshotList); when(awsMock.region()).thenReturn("us-east-1"); return awsMock; } private List createSnapshotList(Date startTime) { List snapshotList = new LinkedList(); snapshotList.add(mkSnapshot("snap-12345678901234567", startTime)); snapshotList.add(mkSnapshot("snap-12345678901234567", startTime)); return snapshotList; } private Snapshot mkSnapshot(String snapshotId, Date startTime) { return new Snapshot().withSnapshotId(snapshotId).withState("completed").withStartTime(startTime); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestEBSVolumeJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ //CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws.janitor.crawler; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Date; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import org.testng.Assert; import org.testng.annotations.Test; import com.amazonaws.services.ec2.model.Volume; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; public class TestEBSVolumeJanitorCrawler { @Test public void testResourceTypes() { Date createTime = new Date(); List volumeList = createVolumeList(createTime); EBSVolumeJanitorCrawler crawler = new EBSVolumeJanitorCrawler(createMockAWSClient(volumeList)); EnumSet types = crawler.resourceTypes(); Assert.assertEquals(types.size(), 1); Assert.assertEquals(types.iterator().next().name(), "EBS_VOLUME"); } @Test public void testVolumesWithNullIds() { Date createTime = new Date(); List volumeList = createVolumeList(createTime); EBSVolumeJanitorCrawler crawler = new EBSVolumeJanitorCrawler(createMockAWSClient(volumeList)); List resources = crawler.resources(); verifyVolumeList(resources, volumeList, createTime); } @Test public void testVolumesWithIds() { Date createTime = new Date(); List volumeList = createVolumeList(createTime); String[] ids = {"vol-12345678901234567", "vol-12345678901234567"}; EBSVolumeJanitorCrawler crawler = new EBSVolumeJanitorCrawler(createMockAWSClient(volumeList, ids)); List resources = crawler.resources(ids); verifyVolumeList(resources, volumeList, createTime); } @Test public void testVolumesWithResourceType() { Date createTime = new Date(); List volumeList = createVolumeList(createTime); EBSVolumeJanitorCrawler crawler = new EBSVolumeJanitorCrawler(createMockAWSClient(volumeList)); for (AWSResourceType resourceType : AWSResourceType.values()) { List resources = crawler.resources(resourceType); if (resourceType == AWSResourceType.EBS_VOLUME) { verifyVolumeList(resources, volumeList, createTime); } else { Assert.assertTrue(resources.isEmpty()); } } } private void verifyVolumeList(List resources, List volumeList, Date createTime) { Assert.assertEquals(resources.size(), volumeList.size()); for (int i = 0; i < resources.size(); i++) { Volume volume = volumeList.get(i); verifyVolume(resources.get(i), volume.getVolumeId(), createTime); } } private void verifyVolume(Resource volume, String volumeId, Date createTime) { Assert.assertEquals(volume.getResourceType(), AWSResourceType.EBS_VOLUME); Assert.assertEquals(volume.getId(), volumeId); Assert.assertEquals(volume.getRegion(), "us-east-1"); Assert.assertEquals(((AWSResource) volume).getAWSResourceState(), "available"); Assert.assertEquals(volume.getLaunchTime(), createTime); } private AWSClient createMockAWSClient(List volumeList, String... ids) { AWSClient awsMock = mock(AWSClient.class); when(awsMock.describeVolumes(ids)).thenReturn(volumeList); when(awsMock.region()).thenReturn("us-east-1"); return awsMock; } private List createVolumeList(Date createTime) { List volumeList = new LinkedList(); volumeList.add(mkVolume("vol-12345678901234567", createTime)); volumeList.add(mkVolume("vol-12345678901234567", createTime)); return volumeList; } private Volume mkVolume(String volumeId, Date createTime) { return new Volume().withVolumeId(volumeId).withState("available").withCreateTime(createTime); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestELBJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws.janitor.crawler; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.elasticloadbalancing.model.Instance; import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; import org.apache.commons.lang.math.NumberUtils; import org.testng.Assert; import org.testng.annotations.Test; import java.util.Arrays; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TestELBJanitorCrawler { @Test public void testResourceTypes() { boolean includeInstances = false; AWSClient client = createMockAWSClient(); addELBsToMock(client, createELBList(includeInstances)); ELBJanitorCrawler crawler = new ELBJanitorCrawler(client); EnumSet types = crawler.resourceTypes(); Assert.assertEquals(types.size(), 1); Assert.assertEquals(types.iterator().next().name(), "ELB"); } @Test public void testElbsWithNoInstances() { boolean includeInstances = false; AWSClient client = createMockAWSClient(); List elbs = createELBList(includeInstances); addELBsToMock(client, elbs); ELBJanitorCrawler crawler = new ELBJanitorCrawler(client); List resources = crawler.resources(); verifyELBList(resources, elbs); } @Test public void testElbsWithInstances() { boolean includeInstances = true; AWSClient client = createMockAWSClient(); List elbs = createELBList(includeInstances); addELBsToMock(client, elbs); ELBJanitorCrawler crawler = new ELBJanitorCrawler(client); List resources = crawler.resources(); verifyELBList(resources, elbs); } @Test public void testElbsWithReferencedASGs() { boolean includeInstances = true; boolean includeELbs = true; AWSClient client = createMockAWSClient(); List elbs = createELBList(includeInstances); List asgs = createASGList(includeELbs); addELBsToMock(client, elbs); addASGsToMock(client, asgs); ELBJanitorCrawler crawler = new ELBJanitorCrawler(client); List resources = crawler.resources(); verifyELBList(resources, elbs, 1); } @Test public void testElbsWithNoReferencedASGs() { boolean includeInstances = true; boolean includeELbs = false; AWSClient client = createMockAWSClient(); List elbs = createELBList(includeInstances); List asgs = createASGList(includeELbs); addELBsToMock(client, elbs); addASGsToMock(client, asgs); ELBJanitorCrawler crawler = new ELBJanitorCrawler(client); List resources = crawler.resources(); verifyELBList(resources, elbs, 0); } @Test public void testElbsWithMultipleReferencedASGs() { boolean includeInstances = true; boolean includeELbs = false; AWSClient client = createMockAWSClient(); List elbs = createELBList(includeInstances); List asgs = createASGList(includeELbs); asgs.get(0).setLoadBalancerNames(Arrays.asList("elb1", "elb2")); addELBsToMock(client, elbs); addASGsToMock(client, asgs); ELBJanitorCrawler crawler = new ELBJanitorCrawler(client); List resources = crawler.resources(); verifyELBList(resources, elbs, 1); } private void verifyELBList(List resources, List elbList) { verifyELBList(resources, elbList, 0); } private void verifyELBList(List resources, List elbList, int asgCount) { Assert.assertEquals(resources.size(), elbList.size()); for (int i = 0; i < resources.size(); i++) { LoadBalancerDescription elb = elbList.get(i); verifyELB(resources.get(i), elb, asgCount); } } private void verifyELB(Resource asg, LoadBalancerDescription elb, int asgCount) { Assert.assertEquals(asg.getResourceType(), AWSResourceType.ELB); Assert.assertEquals(asg.getId(), elb.getLoadBalancerName()); Assert.assertEquals(asg.getRegion(), "us-east-1"); int instanceCount = elb.getInstances().size(); int resourceInstanceCount = NumberUtils.toInt(asg.getAdditionalField("instanceCount")); Assert.assertEquals(instanceCount, resourceInstanceCount); int resourceASGCount = NumberUtils.toInt(asg.getAdditionalField("referencedASGCount")); Assert.assertEquals(resourceASGCount, asgCount); } private AWSClient createMockAWSClient() { AWSClient awsMock = mock(AWSClient.class); return awsMock; } private void addELBsToMock(AWSClient awsMock, List elbList, String... elbNames) { when(awsMock.describeElasticLoadBalancers(elbNames)).thenReturn(elbList); when(awsMock.region()).thenReturn("us-east-1"); } private void addASGsToMock(AWSClient awsMock, List asgList) { when(awsMock.describeAutoScalingGroups()).thenReturn(asgList); when(awsMock.region()).thenReturn("us-east-1"); } private List createELBList(boolean includeInstances) { List elbList = new LinkedList<>(); elbList.add(mkELB("elb1", includeInstances)); elbList.add(mkELB("elb2", includeInstances)); return elbList; } private LoadBalancerDescription mkELB(String elbName, boolean includeInstances) { LoadBalancerDescription elb = new LoadBalancerDescription().withLoadBalancerName(elbName); if (includeInstances) { List instances = new LinkedList<>(); Instance i1 = new Instance().withInstanceId("i-000001"); Instance i2 = new Instance().withInstanceId("i-000002"); elb.setInstances(instances); } return elb; } private List createASGList(boolean includeElbs) { List asgList = new LinkedList(); if (includeElbs) { asgList.add(mkASG("asg1", "elb1")); asgList.add(mkASG("asg2", "elb2")); } else { asgList.add(mkASG("asg1", null)); asgList.add(mkASG("asg2", null)); } return asgList; } private AutoScalingGroup mkASG(String asgName, String elb) { AutoScalingGroup asg = new AutoScalingGroup().withAutoScalingGroupName(asgName); asg.setLoadBalancerNames(Arrays.asList(elb)); return asg; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestInstanceJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws.janitor.crawler; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Collections; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import org.testng.Assert; import org.testng.annotations.Test; import com.amazonaws.services.autoscaling.model.AutoScalingInstanceDetails; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.InstanceState; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; public class TestInstanceJanitorCrawler { @Test public void testResourceTypes() { List instanceDetailsList = createInstanceDetailsList(); List instanceList = createInstanceList(); InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(createMockAWSClient( instanceDetailsList, instanceList)); EnumSet types = crawler.resourceTypes(); Assert.assertEquals(types.size(), 1); Assert.assertEquals(types.iterator().next().name(), "INSTANCE"); } @Test public void testInstancesWithNullIds() { List instanceDetailsList = createInstanceDetailsList(); List instanceList = createInstanceList(); AWSClient awsMock = createMockAWSClient(instanceDetailsList, instanceList); InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(awsMock); List resources = crawler.resources(); verifyInstanceList(resources, instanceDetailsList); } @Test public void testInstancesWithIds() { List instanceDetailsList = createInstanceDetailsList(); List instanceList = createInstanceList(); String[] ids = {"i-12345678901234560", "i-12345678901234561"}; AWSClient awsMock = createMockAWSClient(instanceDetailsList, instanceList, ids); InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(awsMock); List resources = crawler.resources(ids); verifyInstanceList(resources, instanceDetailsList); } @Test public void testInstancesWithResourceType() { List instanceDetailsList = createInstanceDetailsList(); List instanceList = createInstanceList(); AWSClient awsMock = createMockAWSClient(instanceDetailsList, instanceList); InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(awsMock); for (AWSResourceType resourceType : AWSResourceType.values()) { List resources = crawler.resources(resourceType); if (resourceType == AWSResourceType.INSTANCE) { verifyInstanceList(resources, instanceDetailsList); } else { Assert.assertTrue(resources.isEmpty()); } } } @Test public void testInstancesNotExistingInASG() { List instanceDetailsList = Collections.emptyList(); List instanceList = createInstanceList(); AWSClient awsMock = createMockAWSClient(instanceDetailsList, instanceList); InstanceJanitorCrawler crawler = new InstanceJanitorCrawler(awsMock); List resources = crawler.resources(); Assert.assertEquals(resources.size(), instanceList.size()); } private void verifyInstanceList(List resources, List instanceList) { Assert.assertEquals(resources.size(), instanceList.size()); for (int i = 0; i < resources.size(); i++) { AutoScalingInstanceDetails instance = instanceList.get(i); verifyInstance(resources.get(i), instance.getInstanceId(), instance.getAutoScalingGroupName()); } } private void verifyInstance(Resource instance, String instanceId, String asgName) { Assert.assertEquals(instance.getResourceType(), AWSResourceType.INSTANCE); Assert.assertEquals(instance.getId(), instanceId); Assert.assertEquals(instance.getRegion(), "us-east-1"); Assert.assertEquals(instance.getAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME), asgName); Assert.assertEquals(((AWSResource) instance).getAWSResourceState(), "running"); } private AWSClient createMockAWSClient(List instanceDetailsList, List instanceList, String... ids) { AWSClient awsMock = mock(AWSClient.class); when(awsMock.describeAutoScalingInstances(ids)).thenReturn(instanceDetailsList); when(awsMock.describeInstances(ids)).thenReturn(instanceList); when(awsMock.region()).thenReturn("us-east-1"); return awsMock; } private List createInstanceDetailsList() { List instanceList = new LinkedList(); instanceList.add(mkInstanceDetails("i-12345678901234560", "asg1")); instanceList.add(mkInstanceDetails("i-12345678901234561", "asg2")); return instanceList; } private AutoScalingInstanceDetails mkInstanceDetails(String instanceId, String asgName) { return new AutoScalingInstanceDetails().withInstanceId(instanceId).withAutoScalingGroupName(asgName); } private List createInstanceList() { List instanceList = new LinkedList(); instanceList.add(mkInstance("i-12345678901234560")); instanceList.add(mkInstance("i-12345678901234561")); return instanceList; } private Instance mkInstance(String instanceId) { return new Instance().withInstanceId(instanceId) .withState(new InstanceState().withName("running")); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/crawler/TestLaunchConfigJanitorCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.aws.janitor.crawler; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.LaunchConfiguration; import com.google.common.collect.Lists; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.client.aws.AWSClient; import org.apache.commons.lang.Validate; import org.testng.Assert; import org.testng.annotations.Test; import java.util.EnumSet; import java.util.List; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TestLaunchConfigJanitorCrawler { @Test public void testResourceTypes() { int n = 2; String[] lcNames = {"launchConfig1", "launchConfig2"}; LaunchConfigJanitorCrawler crawler = new LaunchConfigJanitorCrawler(createMockAWSClient( createASGList(n), createLaunchConfigList(n), lcNames)); EnumSet types = crawler.resourceTypes(); Assert.assertEquals(types.size(), 1); Assert.assertEquals(types.iterator().next().name(), "LAUNCH_CONFIG"); } @Test public void testInstancesWithNullNames() { int n = 2; List lcList = createLaunchConfigList(n); LaunchConfigJanitorCrawler crawler = new LaunchConfigJanitorCrawler(createMockAWSClient( createASGList(n), lcList)); List resources = crawler.resources(); verifyLaunchConfigList(resources, lcList); } @Test public void testInstancesWithNames() { int n = 2; String[] lcNames = {"launchConfig1", "launchConfig2"}; List lcList = createLaunchConfigList(n); LaunchConfigJanitorCrawler crawler = new LaunchConfigJanitorCrawler(createMockAWSClient( createASGList(n), lcList, lcNames)); List resources = crawler.resources(lcNames); verifyLaunchConfigList(resources, lcList); } @Test public void testInstancesWithResourceType() { int n = 2; List lcList = createLaunchConfigList(n); LaunchConfigJanitorCrawler crawler = new LaunchConfigJanitorCrawler(createMockAWSClient( createASGList(n), lcList)); for (AWSResourceType resourceType : AWSResourceType.values()) { List resources = crawler.resources(resourceType); if (resourceType == AWSResourceType.LAUNCH_CONFIG) { verifyLaunchConfigList(resources, lcList); } else { Assert.assertTrue(resources.isEmpty()); } } } private void verifyLaunchConfigList(List resources, List lcList) { Assert.assertEquals(resources.size(), lcList.size()); for (int i = 0; i < resources.size(); i++) { LaunchConfiguration lc = lcList.get(i); if (i % 2 == 0) { verifyLaunchConfig(resources.get(i), lc.getLaunchConfigurationName(), true); } else { verifyLaunchConfig(resources.get(i), lc.getLaunchConfigurationName(), null); } } } private void verifyLaunchConfig(Resource launchConfig, String lcName, Boolean usedByASG) { Assert.assertEquals(launchConfig.getResourceType(), AWSResourceType.LAUNCH_CONFIG); Assert.assertEquals(launchConfig.getId(), lcName); Assert.assertEquals(launchConfig.getRegion(), "us-east-1"); if (usedByASG != null) { Assert.assertEquals(launchConfig.getAdditionalField( LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG), usedByASG.toString()); } } private AWSClient createMockAWSClient(List asgList, List lcList, String... lcNames) { AWSClient awsMock = mock(AWSClient.class); when(awsMock.describeAutoScalingGroups()).thenReturn(asgList); when(awsMock.describeLaunchConfigurations(lcNames)).thenReturn(lcList); when(awsMock.region()).thenReturn("us-east-1"); return awsMock; } private List createLaunchConfigList(int n) { List lcList = Lists.newArrayList(); for (int i = 1; i <= n; i++) { lcList.add(mkLaunchConfig("launchConfig" + i)); } return lcList; } private LaunchConfiguration mkLaunchConfig(String lcName) { return new LaunchConfiguration().withLaunchConfigurationName(lcName); } private List createASGList(int n) { Validate.isTrue(n > 0); List asgList = Lists.newArrayList(); for (int i = 1; i <= n; i += 2) { asgList.add(mkASG("asg" + i, "launchConfig" + i)); } return asgList; } private AutoScalingGroup mkASG(String asgName, String lcName) { return new AutoScalingGroup().withAutoScalingGroupName(asgName).withLaunchConfigurationName(lcName); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/TestMonkeyCalendar.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule; import java.util.Calendar; import java.util.Date; import org.joda.time.DateTime; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyCalendar; /** * The class is an implementation of MonkeyCalendar that can always run and * considers calendar days only when calculating the termination date. * */ public class TestMonkeyCalendar implements MonkeyCalendar { @Override public boolean isMonkeyTime(Monkey monkey) { return true; } @Override public int openHour() { return 0; } @Override public int closeHour() { return 24; } @Override public Calendar now() { return Calendar.getInstance(); } @Override public Date getBusinessDay(Date date, int n) { DateTime target = new DateTime(date.getTime()).plusDays(n); return new Date(target.getMillis()); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/asg/TestOldEmptyASGRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.asg; import java.util.Date; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.TestUtils; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; public class TestOldEmptyASGRule { @Test public void testEmptyASGWithObsoleteLaunchConfig() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, "launchConfig"); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); int launchConfiguAgeThreshold = 60; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME, String.valueOf(now.minusDays(launchConfiguAgeThreshold + 1).getMillis())); int retentionDays = 3; OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDays, now); } @Test public void testEmptyASGWithValidLaunchConfig() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, "launchConfig"); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); int launchConfiguAgeThreshold = 60; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME, String.valueOf(now.minusDays(launchConfiguAgeThreshold - 1).getMillis())); int retentionDays = 3; OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testASGWithInstances() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, "launchConfig"); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "2"); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES, "123456789012345671,i-123456789012345672"); int launchConfiguAgeThreshold = 60; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME, String.valueOf(now.minusDays(launchConfiguAgeThreshold + 1).getMillis())); int retentionDays = 3; OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testASGWithoutInstanceAndNonZeroSize() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, "launchConfig"); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "2"); int launchConfiguAgeThreshold = 60; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME, String.valueOf(now.minusDays(launchConfiguAgeThreshold + 1).getMillis())); int retentionDays = 3; OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testEmptyASGWithoutLaunchConfig() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); int launchConfiguAgeThreshold = 60; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); int retentionDays = 3; OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDays, now); } @Test public void testEmptyASGWithLaunchConfigWithoutCreateTime() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, "launchConfig"); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); int launchConfiguAgeThreshold = 60; MonkeyCalendar calendar = new TestMonkeyCalendar(); int retentionDays = 3; OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testResourceWithExpectedTerminationTimeSet() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); int launchConfiguAgeThreshold = 60; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); int retentionDays = 3; OldEmptyASGRule rule = new OldEmptyASGRule(calendar, launchConfiguAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Date oldTermDate = new Date(now.plusDays(10).getMillis()); String oldTermReason = "Foo"; resource.setExpectedTerminationTime(oldTermDate); resource.setTerminationReason(oldTermReason); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime()); Assert.assertEquals(oldTermReason, resource.getTerminationReason()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullValidator() { new OldEmptyASGRule(new TestMonkeyCalendar(), 3, 60, null); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullResource() { OldEmptyASGRule rule = new OldEmptyASGRule(new TestMonkeyCalendar(), 3, 60, new DummyASGInstanceValidator()); rule.isValid(null); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeRetentionDays() { new OldEmptyASGRule(new TestMonkeyCalendar(), -1, 60, new DummyASGInstanceValidator()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeLaunchConfigAgeThreshold() { new OldEmptyASGRule(new TestMonkeyCalendar(), 3, -1, new DummyASGInstanceValidator()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullCalendar() { new OldEmptyASGRule(null, 3, 60, new DummyASGInstanceValidator()); } @Test public void testNonASGResource() { Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE); OldEmptyASGRule rule = new OldEmptyASGRule(new TestMonkeyCalendar(), 3, 60, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/asg/TestSuspendedASGRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ //CHECKSTYLE IGNORE Javadoc //CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.asg; import java.util.Date; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.TestUtils; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.crawler.ASGJanitorCrawler; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; public class TestSuspendedASGRule { @Test public void testEmptyASGSuspendedMoreThanThreshold() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); int suspensionAgeThreshold = 2; DateTime suspensionTime = now.minusDays(suspensionAgeThreshold + 1); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME, ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.print(suspensionTime)); int retentionDays = 3; SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDays, now); } @Test public void testEmptyASGSuspendedLessThanThreshold() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_NAME, "launchConfig"); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); int suspensionAgeThreshold = 2; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_LC_CREATION_TIME, String.valueOf(now.minusDays(suspensionAgeThreshold + 1).getMillis())); int retentionDays = 3; SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testASGWithInstances() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "2"); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_INSTANCES, "123456789012345671,i-123456789012345672"); int suspensionAgeThreshold = 2; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); DateTime suspensionTime = now.minusDays(suspensionAgeThreshold + 1); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME, ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.print(suspensionTime)); int retentionDays = 3; SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testASGWithoutInstanceAndNonZeroSize() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "2"); int suspensionAgeThreshold = 2; MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); DateTime suspensionTime = now.minusDays(suspensionAgeThreshold + 1); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME, ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.print(suspensionTime)); int retentionDays = 3; SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testEmptyASGNotSuspended() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); int suspensionAgeThreshold = 2; MonkeyCalendar calendar = new TestMonkeyCalendar(); int retentionDays = 3; SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testResourceWithExpectedTerminationTimeSet() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); MonkeyCalendar calendar = new TestMonkeyCalendar(); DateTime now = new DateTime(calendar.now().getTimeInMillis()); int suspensionAgeThreshold = 2; DateTime suspensionTime = now.minusDays(suspensionAgeThreshold + 1); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME, ASGJanitorCrawler.SUSPENSION_TIME_FORMATTER.print(suspensionTime)); int retentionDays = 3; SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Date oldTermDate = new Date(now.plusDays(10).getMillis()); String oldTermReason = "Foo"; resource.setExpectedTerminationTime(oldTermDate); resource.setTerminationReason(oldTermReason); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime()); Assert.assertEquals(oldTermReason, resource.getTerminationReason()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullResource() { SuspendedASGRule rule = new SuspendedASGRule(new TestMonkeyCalendar(), 3, 2, new DummyASGInstanceValidator()); rule.isValid(null); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullValidator() { new SuspendedASGRule(new TestMonkeyCalendar(), 3, 2, null); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeRetentionDays() { new SuspendedASGRule(new TestMonkeyCalendar(), -1, 2, new DummyASGInstanceValidator()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeLaunchConfigAgeThreshold() { new SuspendedASGRule(new TestMonkeyCalendar(), 3, -1, new DummyASGInstanceValidator()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testSuspensionTimeIncorrectFormat() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_MAX_SIZE, "0"); MonkeyCalendar calendar = new TestMonkeyCalendar(); int suspensionAgeThreshold = 2; resource.setAdditionalField(ASGJanitorCrawler.ASG_FIELD_SUSPENSION_TIME, "foo"); int retentionDays = 3; SuspendedASGRule rule = new SuspendedASGRule(calendar, suspensionAgeThreshold, retentionDays, new DummyASGInstanceValidator()); Assert.assertFalse(rule.isValid(resource)); } @Test public void testNonASGResource() { Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE); SuspendedASGRule rule = new SuspendedASGRule(new TestMonkeyCalendar(), 3, 2, new DummyASGInstanceValidator()); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullCalendar() { new SuspendedASGRule(null, 3, 2, null); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/elb/TestOrphanedELBRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.elb; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.TestUtils; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; public class TestOrphanedELBRule { @Test public void testELBWithNoInstancesNoASGs() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("test-elb").withResourceType(AWSResourceType.ELB) .withOwnerEmail("owner@foo.com"); resource.setAdditionalField("referencedASGCount", "0"); resource.setAdditionalField("instanceCount", "0"); OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, 7, now); } @Test public void testELBWithInstancesNoASGs() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("test-elb").withResourceType(AWSResourceType.ELB) .withOwnerEmail("owner@foo.com"); resource.setAdditionalField("referencedASGCount", "0"); resource.setAdditionalField("instanceCount", "4"); OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7); Assert.assertTrue(rule.isValid(resource)); } @Test public void testELBWithReferencedASGsNoInstances() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("test-elb").withResourceType(AWSResourceType.ELB) .withOwnerEmail("owner@foo.com"); resource.setAdditionalField("referencedASGCount", "4"); resource.setAdditionalField("instanceCount", "0"); OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7); Assert.assertTrue(rule.isValid(resource)); } @Test public void testMissingInstanceCountCheck() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("test-elb").withResourceType(AWSResourceType.ELB) .withOwnerEmail("owner@foo.com"); resource.setAdditionalField("referencedASGCount", "0"); OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7); Assert.assertTrue(rule.isValid(resource)); } @Test public void testMissingReferencedASGCountCheck() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("test-elb").withResourceType(AWSResourceType.ELB) .withOwnerEmail("owner@foo.com"); resource.setAdditionalField("instanceCount", "0"); OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7); Assert.assertTrue(rule.isValid(resource)); } @Test public void testMissingCountsCheck() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("test-elb").withResourceType(AWSResourceType.ELB) .withOwnerEmail("owner@foo.com"); OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7); Assert.assertTrue(rule.isValid(resource)); } @Test public void testMissingCountsCheckWithExtraFields() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("test-elb").withResourceType(AWSResourceType.ELB) .withOwnerEmail("owner@foo.com"); resource.setAdditionalField("bogusField1", "0"); resource.setAdditionalField("bogusField2", "0"); OrphanedELBRule rule = new OrphanedELBRule(new TestMonkeyCalendar(), 7); Assert.assertTrue(rule.isValid(resource)); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/generic/TestTagValueExclusionRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.generic; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import org.testng.Assert; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; import java.util.HashMap; public class TestTagValueExclusionRule { HashMap exclusionTags = null; @BeforeTest public void beforeTest() { exclusionTags = new HashMap<>(); exclusionTags.put("tag1", "excludeme"); exclusionTags.put("tag2", "excludeme2"); } @Test public void testExcludeTaggedResourceWithTagAndValueMatch1() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); r1.setTag("tag", null); r1.setTag("tag1", "excludeme"); r1.setTag("tag2", "somethingelse"); TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags); Assert.assertTrue(rule.isValid(r1)); } @Test public void testExcludeTaggedResourceWithTagAndValueMatch2() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); r1.setTag("tag", null); r1.setTag("tag1", "somethingelse"); r1.setTag("tag2", "excludeme2"); TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags); Assert.assertTrue(rule.isValid(r1)); } @Test public void testExcludeTaggedResourceWithTagAndValueMatchBoth() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); r1.setTag("tag", null); r1.setTag("tag1", "excludeme"); r1.setTag("tag2", "excludeme2"); TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags); Assert.assertTrue(rule.isValid(r1)); } @Test public void testExcludeTaggedResourceTagMatchOnly() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); r1.setTag("tag", null); r1.setTag("tag1", "somethingelse"); r1.setTag("tag2", "somethingelse2"); TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags); Assert.assertFalse(rule.isValid(r1)); } @Test public void testExcludeTaggedResourceAllNullTags() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); r1.setTag("tag", null); r1.setTag("tag1", null); r1.setTag("tag2", null); TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags); Assert.assertFalse(rule.isValid(r1)); } @Test public void testExcludeTaggedResourceValueMatchOnly() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); r1.setTag("tag", null); r1.setTag("tagA", "excludeme"); r1.setTag("tagB", "excludeme2"); TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags); Assert.assertFalse(rule.isValid(r1)); } @Test public void testExcludeUntaggedResource() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); TagValueExclusionRule rule = new TagValueExclusionRule(exclusionTags); Assert.assertFalse(rule.isValid(r1)); } @Test public void testNameValueConstructor() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); r1.setTag("tag1", "excludeme"); String names = "tag1"; String vals = "excludeme"; TagValueExclusionRule rule = new TagValueExclusionRule(names.split(","), vals.split(",")); Assert.assertTrue(rule.isValid(r1)); } @Test public void testNameValueConstructor2() { Resource r1 = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE).withOwnerEmail("owner@foo.com"); r1.setTag("tag1", "excludeme"); String names = "tag1,tag2"; String vals = "excludeme,excludeme2"; TagValueExclusionRule rule = new TagValueExclusionRule(names.split(","), vals.split(",")); Assert.assertTrue(rule.isValid(r1)); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/generic/TestUntaggedRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.generic; import java.util.Date; import java.util.HashSet; import java.util.Set; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.TestUtils; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; public class TestUntaggedRule { @Test public void testUntaggedInstanceWithOwner() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE) .withOwnerEmail("owner@foo.com"); resource.setTag("tag1", "value1"); ((AWSResource) resource).setAWSResourceState("running"); Set tags = new HashSet(); tags.add("tag1"); tags.add("tag2"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDaysWithOwner, now); } @Test public void testUntaggedInstanceWithoutOwner() { DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE); resource.setTag("tag1", "value1"); ((AWSResource) resource).setAWSResourceState("running"); Set tags = new HashSet(); tags.add("tag1"); tags.add("tag2"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDaysWithoutOwner, now); } @Test public void testTaggedInstance() { Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE); resource.setTag("tag1", "value1"); resource.setTag("tag2", "value2"); ((AWSResource) resource).setAWSResourceState("running"); Set tags = new HashSet(); tags.add("tag1"); tags.add("tag2"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertTrue(rule.isValid(resource)); } @Test public void testUntaggedResource() { DateTime now = DateTime.now(); Resource imageResource = new AWSResource().withId("ami-123123").withResourceType(AWSResourceType.IMAGE); Resource asgResource = new AWSResource().withId("my-cool-asg").withResourceType(AWSResourceType.ASG); Resource ebsSnapshotResource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT); Resource lauchConfigurationResource = new AWSResource().withId("my-cool-launch-configuration").withResourceType(AWSResourceType.LAUNCH_CONFIG); Set tags = new HashSet(); tags.add("tag1"); tags.add("tag2"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertFalse(rule.isValid(imageResource)); Assert.assertFalse(rule.isValid(asgResource)); Assert.assertFalse(rule.isValid(ebsSnapshotResource)); Assert.assertFalse(rule.isValid(lauchConfigurationResource)); TestUtils.verifyTerminationTimeRough(imageResource, retentionDaysWithoutOwner, now); TestUtils.verifyTerminationTimeRough(asgResource, retentionDaysWithoutOwner, now); TestUtils.verifyTerminationTimeRough(ebsSnapshotResource, retentionDaysWithoutOwner, now); TestUtils.verifyTerminationTimeRough(lauchConfigurationResource, retentionDaysWithoutOwner, now); } @Test public void testResourceWithExpectedTerminationTimeSet() { DateTime now = DateTime.now(); Date oldTermDate = new Date(now.plusDays(10).getMillis()); String oldTermReason = "Foo"; Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE) .withExpectedTerminationTime(oldTermDate) .withTerminationReason(oldTermReason); ((AWSResource) resource).setAWSResourceState("running"); Set tags = new HashSet(); tags.add("tag1"); tags.add("tag2"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; UntaggedRule rule = new UntaggedRule(new TestMonkeyCalendar(), tags, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime()); Assert.assertEquals(oldTermReason, resource.getTerminationReason()); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/instance/TestOrphanedInstanceRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.instance; import java.util.Date; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.TestUtils; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.crawler.InstanceJanitorCrawler; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; public class TestOrphanedInstanceRule { @Test public void testOrphanedInstancesWithOwner() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())) .withOwnerEmail("owner@foo.com"); ((AWSResource) resource).setAWSResourceState("running"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDaysWithOwner, now); } @Test public void testOrphanedInstancesWithoutOwner() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("running"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDaysWithoutOwner, now); } @Test public void testOrphanedInstancesWithoutLaunchTime() { int ageThreshold = 5; Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE); ((AWSResource) resource).setAWSResourceState("running"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testOrphanedInstancesWithLaunchTimeNotExpires() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE) .withLaunchTime(new Date(now.minusDays(ageThreshold - 1).getMillis())); ((AWSResource) resource).setAWSResourceState("running"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testNonOrphanedInstances() { int ageThreshold = 5; Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE) .setAdditionalField(InstanceJanitorCrawler.INSTANCE_FIELD_ASG_NAME, "asg1"); ((AWSResource) resource).setAWSResourceState("running"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testResourceWithExpectedTerminationTimeSet() { DateTime now = DateTime.now(); Date oldTermDate = new Date(now.plusDays(10).getMillis()); String oldTermReason = "Foo"; int ageThreshold = 5; Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())) .withExpectedTerminationTime(oldTermDate) .withTerminationReason(oldTermReason); ((AWSResource) resource).setAWSResourceState("running"); int retentionDaysWithOwner = 4; int retentionDaysWithoutOwner = 8; OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), ageThreshold, retentionDaysWithOwner, retentionDaysWithoutOwner); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime()); Assert.assertEquals(oldTermReason, resource.getTerminationReason()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullResource() { OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), 5, 4, 8); rule.isValid(null); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeAgeThreshold() { new OrphanedInstanceRule(new TestMonkeyCalendar(), -1, 4, 8); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeRetentionDaysWithOwner() { new OrphanedInstanceRule(new TestMonkeyCalendar(), 5, -4, 8); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeRetentionDaysWithoutOwner() { new OrphanedInstanceRule(new TestMonkeyCalendar(), 5, 4, -8); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullCalendar() { new OrphanedInstanceRule(null, 5, 4, 8); } @Test public void testNonInstanceResource() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); ((AWSResource) resource).setAWSResourceState("running"); OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), 0, 0, 0); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testNonRunningInstance() { Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE); ((AWSResource) resource).setAWSResourceState("stopping"); OrphanedInstanceRule rule = new OrphanedInstanceRule(new TestMonkeyCalendar(), 0, 0, 0); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/launchconfig/TestOldUnusedLaunchConfigRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.launchconfig; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.TestUtils; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.crawler.LaunchConfigJanitorCrawler; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; import java.util.Date; public class TestOldUnusedLaunchConfigRule { @Test public void testOldUnsedLaunchConfig() { Resource resource = new AWSResource().withId("launchConfig1").withResourceType(AWSResourceType.LAUNCH_CONFIG); resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, "false"); MonkeyCalendar calendar = new TestMonkeyCalendar(); int ageThreshold = 3; DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); int retentionDays = 3; OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDays, now); } @Test public void testOldLaunchConfigWithNullFlag() { Resource resource = new AWSResource().withId("launchConfig1").withResourceType(AWSResourceType.LAUNCH_CONFIG); MonkeyCalendar calendar = new TestMonkeyCalendar(); int ageThreshold = 3; DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); int retentionDays = 3; OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testUnsedLaunchConfigNotOld() { Resource resource = new AWSResource().withId("launchConfig1").withResourceType(AWSResourceType.LAUNCH_CONFIG); resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, "false"); MonkeyCalendar calendar = new TestMonkeyCalendar(); int ageThreshold = 3; DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setLaunchTime(new Date(now.minusDays(ageThreshold - 1).getMillis())); int retentionDays = 3; OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testUsedLaunchConfig() { Resource resource = new AWSResource().withId("launchConfig1").withResourceType(AWSResourceType.LAUNCH_CONFIG); resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, "true"); MonkeyCalendar calendar = new TestMonkeyCalendar(); int ageThreshold = 3; DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); int retentionDays = 3; OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testUsedLaunchConfigNoLaunchTimeSet() { Resource resource = new AWSResource().withId("launchConfig1").withResourceType(AWSResourceType.LAUNCH_CONFIG); resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, "true"); MonkeyCalendar calendar = new TestMonkeyCalendar(); int ageThreshold = 3; int retentionDays = 3; OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testResourceWithExpectedTerminationTimeSet() { Resource resource = new AWSResource().withId("launchConfig1").withResourceType(AWSResourceType.LAUNCH_CONFIG); resource.setAdditionalField(LaunchConfigJanitorCrawler.LAUNCH_CONFIG_FIELD_USED_BY_ASG, "false"); MonkeyCalendar calendar = new TestMonkeyCalendar(); int ageThreshold = 3; DateTime now = new DateTime(calendar.now().getTimeInMillis()); resource.setLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); Date oldTermDate = new Date(now.minusDays(10).getMillis()); String oldTermReason = "Foo"; resource.setExpectedTerminationTime(oldTermDate); resource.setTerminationReason(oldTermReason); int retentionDays = 3; OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(calendar, ageThreshold, retentionDays); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime()); Assert.assertEquals(oldTermReason, resource.getTerminationReason()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullResource() { OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(new TestMonkeyCalendar(), 3, 3); rule.isValid(null); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeRetentionDays() { new OldUnusedLaunchConfigRule(new TestMonkeyCalendar(), -1, 60); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeLaunchConfigAgeThreshold() { new OldUnusedLaunchConfigRule(new TestMonkeyCalendar(), 3, -1); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullCalendar() { new OldUnusedLaunchConfigRule(null, 3, 60); } @Test public void testNonLaunchConfigResource() { Resource resource = new AWSResource().withId("i-12345678901234567").withResourceType(AWSResourceType.INSTANCE); OldUnusedLaunchConfigRule rule = new OldUnusedLaunchConfigRule(new TestMonkeyCalendar(), 3, 60); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/snapshot/TestNoGeneratedAMIRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ //CHECKSTYLE IGNORE Javadoc //CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.snapshot; import java.util.Date; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.TestUtils; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.crawler.EBSSnapshotJanitorCrawler; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; import com.netflix.simianarmy.janitor.JanitorMonkey; public class TestNoGeneratedAMIRule { @Test public void testNonSnapshotResource() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); ((AWSResource) resource).setAWSResourceState("completed"); NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), 0, 0); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testUncompletedVolume() { Resource resource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT); ((AWSResource) resource).setAWSResourceState("stopped"); NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), 0, 0); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testTaggedAsNotMark() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("completed"); int retentionDays = 4; NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); resource.setTag(JanitorMonkey.JANITOR_TAG, "donotmark"); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testUserSpecifiedTerminationDate() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("completed"); int retentionDays = 4; DateTime userDate = new DateTime(now.plusDays(3).withTimeAtStartOfDay()); resource.setTag(JanitorMonkey.JANITOR_TAG, NoGeneratedAMIRule.TERMINATION_DATE_FORMATTER.print(userDate)); NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(resource.getExpectedTerminationTime().getTime(), userDate.getMillis()); } @Test public void testOldSnapshotWithoutAMI() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("completed"); int retentionDays = 4; NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDays, now); } @Test public void testSnapshotWithoutAMINotOld() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(now.minusDays(ageThreshold - 1).getMillis())); ((AWSResource) resource).setAWSResourceState("completed"); int retentionDays = 4; NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testWithAMIs() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("completed"); resource.setAdditionalField(EBSSnapshotJanitorCrawler.SNAPSHOT_FIELD_AMIS, "ami-123"); int retentionDays = 4; NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testSnapshotsWithoutLauchTime() { int ageThreshold = 5; Resource resource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT); ((AWSResource) resource).setAWSResourceState("completed"); int retentionDays = 4; NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testResourceWithExpectedTerminationTimeSet() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("snap-12345678901234567").withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("completed"); Date oldTermDate = new Date(now.plusDays(10).getMillis()); String oldTermReason = "Foo"; int retentionDays = 4; NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); resource.setExpectedTerminationTime(oldTermDate); resource.setTerminationReason(oldTermReason); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime()); Assert.assertEquals(oldTermReason, resource.getTerminationReason()); } @Test public void testOldSnapshotWithoutAMIWithOwnerOverride() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("snap-12345678901234567").withOwnerEmail("owner@netflix.com").withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("completed"); int retentionDays = 4; NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays, "new_owner@netflix.com"); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(resource.getOwnerEmail(), "new_owner@netflix.com"); TestUtils.verifyTerminationTimeRough(resource, retentionDays, now); } @Test public void testOldSnapshotWithoutAMIWithoutOwnerOverride() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("snap-12345678901234567").withOwnerEmail("owner@netflix.com").withResourceType(AWSResourceType.EBS_SNAPSHOT) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("completed"); int retentionDays = 4; NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(resource.getOwnerEmail(), "owner@netflix.com"); TestUtils.verifyTerminationTimeRough(resource, retentionDays, now); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullResource() { NoGeneratedAMIRule rule = new NoGeneratedAMIRule(new TestMonkeyCalendar(), 5, 4); rule.isValid(null); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeAgeThreshold() { new NoGeneratedAMIRule(new TestMonkeyCalendar(), -1, 4); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeRetentionDaysWithOwner() { new NoGeneratedAMIRule(new TestMonkeyCalendar(), 5, -4); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullCalendar() { new NoGeneratedAMIRule(null, 5, 4); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/aws/janitor/rule/volume/TestOldDetachedVolumeRule.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ //CHECKSTYLE IGNORE Javadoc //CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.aws.janitor.rule.volume; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.MonkeyCalendar; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.TestUtils; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.AWSResourceType; import com.netflix.simianarmy.aws.janitor.VolumeTaggingMonkey; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; import com.netflix.simianarmy.janitor.JanitorMonkey; import static org.joda.time.DateTimeConstants.MILLIS_PER_DAY; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; public class TestOldDetachedVolumeRule { @Test public void testNonVolumeResource() { Resource resource = new AWSResource().withId("asg1").withResourceType(AWSResourceType.ASG); ((AWSResource) resource).setAWSResourceState("available"); OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), 0, 0); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testUnavailableVolume() { Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME); ((AWSResource) resource).setAWSResourceState("stopped"); OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), 0, 0); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testTaggedAsNotMark() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("available"); Date lastDetachTime = new Date(now.minusDays(ageThreshold + 1).getMillis()); String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime); resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag); int retentionDays = 4; OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); resource.setTag(JanitorMonkey.JANITOR_TAG, "donotmark"); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testNoMetaTag() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("available"); int retentionDays = 4; OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); resource.setTag(JanitorMonkey.JANITOR_TAG, "donotmark"); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testUserSpecifiedTerminationDate() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("available"); int retentionDays = 4; DateTime userDate = new DateTime(now.plusDays(3).withTimeAtStartOfDay()); resource.setTag(JanitorMonkey.JANITOR_TAG, OldDetachedVolumeRule.TERMINATION_DATE_FORMATTER.print(userDate)); OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(resource.getExpectedTerminationTime().getTime(), userDate.getMillis()); } @Test public void testOldDetachedVolume() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("available"); Date lastDetachTime = new Date(now.minusDays(ageThreshold + 1).getMillis()); String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime); resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag); int retentionDays = 4; OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertFalse(rule.isValid(resource)); TestUtils.verifyTerminationTimeRough(resource, retentionDays, now); } /** This test exists to check logic on a utility method. * The tagging rule for resource expiry uses a variable nubmer of days. * However, JodaTime date arithmetic for DAYS uses calendar days. It does NOT * treat a day as 24 hours in this case (HOUR arithmetic, however, does). * Therefore, a termination policy of 4 days (96 hours) will actually occur in * 95 hours if the resource is tagged with that rule within 4 days of the DST * cutover. * * We experienced test case failures around the 2014 spring DST cutover that * prevented us from getting green builds. So, the assertion logic was loosened * to check that we were within a day of the expected date. For the test case, * all but 4 days of the year this problem never shows up. To verify that our * fix was correct, this test case explicitly sets the date. The other tests * that use a DateTime of "DateTime.now()" are not true unit tests, because the * test does not isolate the date. They are actually a partial integration test, * as they leave the date up to the system where the test executes. * * We have to mock the call to MonkeyCalendar.now() because the constructor * for that class uses Calendar.getInstance() internally. * */ @Test public void testOldDetachedVolumeBeforeDaylightSavingsCutover() { int ageThreshold = 5; //here we set the create date to a few days before a known DST cutover, where //we observed DST failures DateTime closeToSpringAheadDst = new DateTime(2014, 3, 7, 0, 0, DateTimeZone.forID("America/Los_Angeles")); Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(closeToSpringAheadDst.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("available"); Date lastDetachTime = new Date(closeToSpringAheadDst.minusDays(ageThreshold + 1).getMillis()); String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime); resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag); int retentionDays = 4; //set the "now" to the fixed execution date for this rule and create a partial mock Calendar fixed = Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles")); fixed.setTimeInMillis(closeToSpringAheadDst.getMillis()); MonkeyCalendar monkeyCalendar = new TestMonkeyCalendar(); MonkeyCalendar spyCalendar = spy(monkeyCalendar); when(spyCalendar.now()).thenReturn(fixed); //use the partial mock for the OldDetachedVolumeRule OldDetachedVolumeRule rule = new OldDetachedVolumeRule(spyCalendar, ageThreshold, retentionDays); Assert.assertFalse(rule.isValid(resource)); //this volume should be seen as invalid //3.26.2014. Commenting out DST cutover verification. A line in //OldDetachedVolumeRule.isValid actually creates a new date from the Tag value. //while this unit test tries its best to use invariants, the tag does not contain timezone //information, so a time set in the Los Angeles timezone and tagged, then parsed as //UTC (if that's how the VM running the test is set) will fail. ///////////////////////////// //Leaving the code in place to be uncommnented later if that class is refactored //to support a design that promotes more complete testing. //now verify that the difference between "now" and the cutoff is slightly under the intended //retention limit, as the DST cutover makes us lose one hour //verifyDSTCutoverHappened(resource, retentionDays, closeToSpringAheadDst); ///////////////////////////// //now verify that our projected termination time is within one day of what was asked for TestUtils.verifyTerminationTimeRough(resource, retentionDays, closeToSpringAheadDst); } @Test public void testDetachedVolumeNotOld() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("available"); Date lastDetachTime = new Date(now.minusDays(ageThreshold - 1).getMillis()); String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime); resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag); int retentionDays = 4; OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testAttachedVolume() { int ageThreshold = 5; DateTime now = DateTime.now(); Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("available"); String metaTag = VolumeTaggingMonkey.makeMetaTag("i-12345678901234567", "owner", null); resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag); int retentionDays = 4; OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); Assert.assertTrue(rule.isValid(resource)); Assert.assertNull(resource.getExpectedTerminationTime()); } @Test public void testResourceWithExpectedTerminationTimeSet() { DateTime now = DateTime.now(); Date oldTermDate = new Date(now.plusDays(10).getMillis()); String oldTermReason = "Foo"; int ageThreshold = 5; Resource resource = new AWSResource().withId("vol-12345678901234567").withResourceType(AWSResourceType.EBS_VOLUME) .withLaunchTime(new Date(now.minusDays(ageThreshold + 1).getMillis())); ((AWSResource) resource).setAWSResourceState("available"); Date lastDetachTime = new Date(now.minusDays(ageThreshold + 1).getMillis()); String metaTag = VolumeTaggingMonkey.makeMetaTag(null, null, lastDetachTime); resource.setTag(JanitorMonkey.JANITOR_META_TAG, metaTag); int retentionDays = 4; OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), ageThreshold, retentionDays); resource.setExpectedTerminationTime(oldTermDate); resource.setTerminationReason(oldTermReason); Assert.assertFalse(rule.isValid(resource)); Assert.assertEquals(oldTermDate, resource.getExpectedTerminationTime()); Assert.assertEquals(oldTermReason, resource.getTerminationReason()); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullResource() { OldDetachedVolumeRule rule = new OldDetachedVolumeRule(new TestMonkeyCalendar(), 5, 4); rule.isValid(null); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeAgeThreshold() { new OldDetachedVolumeRule(new TestMonkeyCalendar(), -1, 4); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNgativeRetentionDaysWithOwner() { new OldDetachedVolumeRule(new TestMonkeyCalendar(), 5, -4); } @Test(expectedExceptions = IllegalArgumentException.class) public void testNullCalendar() { new OldDetachedVolumeRule(null, 5, 4); } /** Verify that a test conditioned to run across the spring DST cutover actually did * cross that threshold. The real difference will be about 0.05 days less than * the retentionDays parameter. * @param resource The AWS resource being tested * @param retentionDays Number of days the resource should be kept around * @param timeOfCheck When the check is executed */ private void verifyDSTCutoverHappened(Resource resource, int retentionDays, DateTime timeOfCheck) { double realDays = (double) (resource.getExpectedTerminationTime().getTime() - timeOfCheck.getMillis()) / (double) MILLIS_PER_DAY; long days = (resource.getExpectedTerminationTime().getTime() - timeOfCheck.getMillis()) / MILLIS_PER_DAY; Assert.assertTrue(realDays < (double) retentionDays); Assert.assertNotEquals(days, retentionDays); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/TestBasicCalendar.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic; import java.util.Calendar; import java.util.Properties; import java.util.TimeZone; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.TestMonkey; // CHECKSTYLE IGNORE MagicNumberCheck public class TestBasicCalendar extends BasicCalendar { private static final Properties PROPS = new Properties(); private static final BasicConfiguration CFG = new BasicConfiguration(PROPS); public TestBasicCalendar() { super(CFG); } @Test public void testConstructors() { BasicCalendar cal = new BasicCalendar(CFG); Assert.assertEquals(cal.openHour(), 9); Assert.assertEquals(cal.closeHour(), 15); Assert.assertEquals(cal.now().getTimeZone(), TimeZone.getTimeZone("America/Los_Angeles")); cal = new BasicCalendar(11, 12, TimeZone.getTimeZone("Europe/Stockholm")); Assert.assertEquals(cal.openHour(), 11); Assert.assertEquals(cal.closeHour(), 12); Assert.assertEquals(cal.now().getTimeZone(), TimeZone.getTimeZone("Europe/Stockholm")); } private Calendar now = super.now(); @Override public Calendar now() { return (Calendar) now.clone(); } private void setNow(Calendar now) { this.now = now; } @Test void testMonkeyTime() { Calendar test = Calendar.getInstance(); Monkey monkey = new TestMonkey(); // using leap day b/c it is not a holiday & not a weekend test.set(Calendar.YEAR, 2012); test.set(Calendar.MONTH, Calendar.FEBRUARY); test.set(Calendar.DAY_OF_MONTH, 29); test.set(Calendar.HOUR_OF_DAY, 8); // 8am leap day setNow(test); Assert.assertFalse(isMonkeyTime(monkey)); test.set(Calendar.HOUR_OF_DAY, 10); // 10am leap day setNow(test); Assert.assertTrue(isMonkeyTime(monkey)); test.set(Calendar.HOUR_OF_DAY, 17); // 5pm leap day setNow(test); Assert.assertFalse(isMonkeyTime(monkey)); // set to the following Saturday so we can test we dont run on weekends // even though within "business hours" test.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); test.set(Calendar.HOUR_OF_DAY, 10); setNow(test); Assert.assertFalse(isMonkeyTime(monkey)); // test config overrides PROPS.setProperty("simianarmy.calendar.isMonkeyTime", Boolean.toString(true)); Assert.assertTrue(isMonkeyTime(monkey)); PROPS.setProperty("simianarmy.calendar.isMonkeyTime", Boolean.toString(false)); Assert.assertFalse(isMonkeyTime(monkey)); } @DataProvider public Object[][] holidayDataProvider() { return new Object[][] {{Calendar.JANUARY, 2}, // New Year's Day {Calendar.JANUARY, 16}, // MLK day {Calendar.FEBRUARY, 20}, // Washington's Birthday {Calendar.MAY, 28}, // Memorial Day {Calendar.JULY, 4}, // Independence Day {Calendar.SEPTEMBER, 3}, // Labor Day {Calendar.OCTOBER, 8}, // Columbus Day {Calendar.NOVEMBER, 12}, // Veterans Day {Calendar.NOVEMBER, 22}, // Thanksgiving Day {Calendar.DECEMBER, 25} // Christmas Day }; } @Test(dataProvider = "holidayDataProvider") public void testHolidays(int month, int dayOfMonth) { Calendar test = Calendar.getInstance(); test.set(Calendar.YEAR, 2012); test.set(Calendar.MONTH, month); test.set(Calendar.DAY_OF_MONTH, dayOfMonth); test.set(Calendar.HOUR_OF_DAY, 10); setNow(test); Assert.assertTrue(isHoliday(test), test.getTime().toString() + " is a holiday?"); } @Test public void testGetBusinessDayWihoutGap() { // the days from 12/3/2012 to 12/7/2012 are all business days int hour = 10; Calendar test = now(); test.set(Calendar.YEAR, 2012); test.set(Calendar.MONTH, Calendar.DECEMBER); test.set(Calendar.DAY_OF_MONTH, 3); test.set(Calendar.HOUR_OF_DAY, hour); int day = test.get(Calendar.DAY_OF_MONTH); for (int n = 0; n <= 4; n++) { Calendar businessDay = now(); businessDay.setTime(getBusinessDay(test.getTime(), n)); Assert.assertEquals(businessDay.get(Calendar.DAY_OF_MONTH), day + n); Assert.assertEquals(businessDay.get(Calendar.HOUR_OF_DAY), hour); } } @Test public void testGetBusinessDayWihWeekend() { // 12/7/2012 is Friday int hour = 10; Calendar test = now(); test.set(Calendar.YEAR, 2012); test.set(Calendar.MONTH, Calendar.DECEMBER); test.set(Calendar.DAY_OF_MONTH, 7); test.set(Calendar.HOUR_OF_DAY, hour); int day = test.get(Calendar.DAY_OF_MONTH); for (int n = 1; n <= 5; n++) { Calendar businessDay = now(); businessDay.setTime(getBusinessDay(test.getTime(), n)); Assert.assertEquals(businessDay.get(Calendar.DAY_OF_MONTH), day + n + 2); Assert.assertEquals(businessDay.get(Calendar.HOUR_OF_DAY), hour); } } @Test public void testGetBusinessDayWihHoliday() { // 12/23/2012 is Monday and 12/24 - 12/26 are holidays int hour = 10; Calendar test = now(); test.set(Calendar.YEAR, 2012); test.set(Calendar.MONTH, Calendar.DECEMBER); test.set(Calendar.DAY_OF_MONTH, 24); test.set(Calendar.HOUR_OF_DAY, hour); int day = test.get(Calendar.DAY_OF_MONTH); Calendar businessDay = now(); businessDay.setTime(getBusinessDay(test.getTime(), 1)); Assert.assertEquals(businessDay.get(Calendar.DAY_OF_MONTH), day + 4); Assert.assertEquals(businessDay.get(Calendar.HOUR_OF_DAY), hour); } @Test public void testGetBusinessDayWihHolidayNextYear() { // 12/28/2012 is Friday and 12/31 - 1/1 are holidays int hour = 10; Calendar test = now(); test.set(Calendar.YEAR, 2012); test.set(Calendar.MONTH, Calendar.DECEMBER); test.set(Calendar.DAY_OF_MONTH, 28); test.set(Calendar.HOUR_OF_DAY, hour); Calendar businessDay = now(); businessDay.setTime(getBusinessDay(test.getTime(), 1)); // The next business day should be 1/2/2013 Assert.assertEquals(businessDay.get(Calendar.YEAR), 2013); Assert.assertEquals(businessDay.get(Calendar.MONTH), Calendar.JANUARY); Assert.assertEquals(businessDay.get(Calendar.DAY_OF_MONTH), 2); Assert.assertEquals(businessDay.get(Calendar.HOUR_OF_DAY), hour); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/TestBasicConfiguration.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic; import org.testng.annotations.Test; import org.testng.Assert; import java.util.Properties; public class TestBasicConfiguration extends BasicConfiguration { private static final Properties PROPS = new Properties(); public TestBasicConfiguration() { super(PROPS); } @Test public void testGetBool() { PROPS.clear(); Assert.assertFalse(getBool("foobar.enabled")); PROPS.setProperty("foobar.enabled", "true"); Assert.assertTrue(getBool("foobar.enabled")); PROPS.setProperty("foobar.enabled", "false"); Assert.assertFalse(getBool("foobar.enabled")); } @Test public void testGetBoolOrElse() { PROPS.clear(); Assert.assertFalse(getBoolOrElse("foobar.enabled", false)); Assert.assertTrue(getBoolOrElse("foobar.enabled", true)); PROPS.setProperty("foobar.enabled", "true"); Assert.assertTrue(getBoolOrElse("foobar.enabled", false)); Assert.assertTrue(getBoolOrElse("foobar.enabled", true)); PROPS.setProperty("foobar.enabled", "false"); Assert.assertFalse(getBoolOrElse("foobar.enabled", false)); Assert.assertFalse(getBoolOrElse("foobar.enabled", true)); } @Test public void testGetNumOrElse() { // CHECKSTYLE IGNORE MagicNumberCheck PROPS.clear(); Assert.assertEquals(getNumOrElse("foobar.number", 42), 42D); PROPS.setProperty("foobar.number", "0"); Assert.assertEquals(getNumOrElse("foobar.number", 42), 0D); } @Test public void testGetStr() { PROPS.clear(); Assert.assertNull(getStr("foobar")); PROPS.setProperty("foobar", "string"); Assert.assertEquals(getStr("foobar"), "string"); } @Test public void testGetStrOrElse() { PROPS.clear(); Assert.assertEquals(getStrOrElse("foobar", "default"), "default"); PROPS.setProperty("foobar", "string"); Assert.assertEquals(getStrOrElse("foobar", "default"), "string"); PROPS.setProperty("foobar", ""); Assert.assertEquals(getStrOrElse("foobar", "default"), ""); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/TestBasicContext.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic; import com.amazonaws.ClientConfiguration; import org.testng.Assert; import org.testng.annotations.Test; public class TestBasicContext { @Test public void testContext() { BasicChaosMonkeyContext ctx = new BasicChaosMonkeyContext(); Assert.assertNotNull(ctx.scheduler()); Assert.assertNotNull(ctx.calendar()); Assert.assertNotNull(ctx.configuration()); Assert.assertNotNull(ctx.cloudClient()); Assert.assertNotNull(ctx.chaosCrawler()); Assert.assertNotNull(ctx.chaosInstanceSelector()); Assert.assertTrue(ctx.configuration().getBool("simianarmy.calendar.isMonkeyTime")); Assert.assertEquals(ctx.configuration().getStr("simianarmy.client.aws.assumeRoleArn"), "arn:aws:iam::fakeAccount:role/fakeRole"); // Verify that the property in chaos.properties overrides the same property in simianarmy.properties Assert.assertFalse(ctx.configuration().getBool("simianarmy.chaos.enabled")); } @Test public void testIsSafeToLogProperty() { BasicChaosMonkeyContext ctx = new BasicChaosMonkeyContext(); Assert.assertTrue(ctx.isSafeToLog("simianarmy.client.aws.region")); } @Test public void testIsNotSafeToLogProperty() { BasicChaosMonkeyContext ctx = new BasicChaosMonkeyContext(); Assert.assertFalse(ctx.isSafeToLog("simianarmy.client.aws.secretKey")); } @Test public void testIsNotSafeToLogVsphereProperty() { BasicChaosMonkeyContext ctx = new BasicChaosMonkeyContext(); Assert.assertFalse(ctx.isSafeToLog("simianarmy.client.vsphere.password")); } @Test public void testIsNotUsingProxyByDefault() { BasicSimianArmyContext ctx = new BasicSimianArmyContext(); ClientConfiguration awsClientConfig = ctx.getAwsClientConfig(); Assert.assertNull(awsClientConfig.getProxyHost()); Assert.assertEquals(awsClientConfig.getProxyPort(), -1); Assert.assertNull(awsClientConfig.getProxyUsername()); Assert.assertNull(awsClientConfig.getProxyPassword()); } @Test public void testIsAbleToUseProxyByConfiguration() { BasicSimianArmyContext ctx = new BasicSimianArmyContext("proxy.properties"); ClientConfiguration awsClientConfig = ctx.getAwsClientConfig(); Assert.assertEquals(awsClientConfig.getProxyHost(), "127.0.0.1"); Assert.assertEquals(awsClientConfig.getProxyPort(), 80); Assert.assertEquals(awsClientConfig.getProxyUsername(), "fakeUser"); Assert.assertEquals(awsClientConfig.getProxyPassword(), "fakePassword"); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/TestBasicMonkeyServer.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.MonkeyRunner; import com.netflix.simianarmy.TestMonkey; import com.netflix.simianarmy.basic.chaos.BasicChaosMonkey; import com.netflix.simianarmy.chaos.TestChaosMonkeyContext; @SuppressWarnings("serial") public class TestBasicMonkeyServer extends BasicMonkeyServer { private static final MonkeyRunner RUNNER = MonkeyRunner.getInstance(); private static boolean monkeyRan = false; public static class SillyMonkey extends TestMonkey { @Override public void doMonkeyBusiness() { monkeyRan = true; } } @Override public void addMonkeysToRun() { MonkeyRunner.getInstance().replaceMonkey(BasicChaosMonkey.class, TestChaosMonkeyContext.class); MonkeyRunner.getInstance().addMonkey(SillyMonkey.class); } @Test public void testServer() { BasicMonkeyServer server = new TestBasicMonkeyServer(); try { server.init(); } catch (Exception e) { Assert.fail("failed to init server", e); } // there is a race condition since the monkeys will run // in a different thread. On some systems we might // need to add a sleep Assert.assertTrue(monkeyRan, "silly monkey ran"); try { server.destroy(); } catch (Exception e) { Assert.fail("failed to destroy server", e); } Assert.assertEquals(RUNNER.getMonkeys().size(), 1); Assert.assertEquals(RUNNER.getMonkeys().get(0).getClass(), SillyMonkey.class); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/TestBasicRecorderEvent.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic; import java.util.HashMap; import java.util.Map; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.MonkeyType; public class TestBasicRecorderEvent { public enum Types implements MonkeyType { MONKEY }; public enum EventTypes implements EventType { EVENT } @Test public void test() { MonkeyType monkeyType = Types.MONKEY; EventType eventType = EventTypes.EVENT; BasicRecorderEvent evt = new BasicRecorderEvent(monkeyType, eventType, "region", "test-id"); testEvent(evt); // CHECKSTYLE IGNORE MagicNumberCheck long time = 1330538400000L; evt = new BasicRecorderEvent(monkeyType, eventType, "region", "test-id", time); testEvent(evt); Assert.assertEquals(evt.eventTime().getTime(), time); } void testEvent(BasicRecorderEvent evt) { Assert.assertEquals(evt.id(), "test-id"); Assert.assertEquals(evt.monkeyType(), Types.MONKEY); Assert.assertEquals(evt.eventType(), EventTypes.EVENT); Assert.assertEquals(evt.region(), "region"); Assert.assertEquals(evt.addField("a", "1"), evt); Map map = new HashMap(); map.put("b", "2"); map.put("c", "3"); Assert.assertEquals(evt.addFields(map), evt); Assert.assertEquals(evt.field("a"), "1"); Assert.assertEquals(evt.field("b"), "2"); Assert.assertEquals(evt.field("c"), "3"); Map f = evt.fields(); Assert.assertEquals(f.get("a"), "1"); Assert.assertEquals(f.get("b"), "2"); Assert.assertEquals(f.get("c"), "3"); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/TestBasicScheduler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic; import static org.mockito.Mockito.when; import static org.mockito.Mockito.mock; import java.util.Calendar; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import org.testng.Assert; import org.testng.annotations.Test; import com.google.common.util.concurrent.Callables; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyType; import com.netflix.simianarmy.TestMonkeyContext; // CHECKSTYLE IGNORE MagicNumber public class TestBasicScheduler { @Test public void testConstructors() { BasicScheduler sched = new BasicScheduler(); Assert.assertNotNull(sched); Assert.assertEquals(sched.frequency(), 1); Assert.assertEquals(sched.frequencyUnit(), TimeUnit.HOURS); BasicScheduler sched2 = new BasicScheduler(12, TimeUnit.MINUTES, 2); Assert.assertEquals(sched2.frequency(), 12); Assert.assertEquals(sched2.frequencyUnit(), TimeUnit.MINUTES); } private enum Enums implements MonkeyType { MONKEY }; private enum EventEnums implements EventType { EVENT } @Test public void testRunner() throws InterruptedException { BasicScheduler sched = new BasicScheduler(200, TimeUnit.MILLISECONDS, 1); Monkey mockMonkey = mock(Monkey.class); when(mockMonkey.context()).thenReturn(new TestMonkeyContext(Enums.MONKEY)); when(mockMonkey.type()).thenReturn(Enums.MONKEY).thenReturn(Enums.MONKEY); final AtomicLong counter = new AtomicLong(0L); sched.start(mockMonkey, new Runnable() { @Override public void run() { counter.incrementAndGet(); } }); Thread.sleep(100); Assert.assertEquals(counter.get(), 1); Thread.sleep(200); Assert.assertEquals(counter.get(), 2); sched.stop(mockMonkey); Thread.sleep(200); Assert.assertEquals(counter.get(), 2); } @Test public void testDelayedStart() throws Exception { BasicScheduler sched = new BasicScheduler(1, TimeUnit.HOURS, 1); TestMonkeyContext context = new TestMonkeyContext(Enums.MONKEY); Monkey mockMonkey = mock(Monkey.class); when(mockMonkey.context()).thenReturn(context).thenReturn(context); when(mockMonkey.type()).thenReturn(Enums.MONKEY).thenReturn(Enums.MONKEY); // first monkey has no previous events, so it runs practically immediately FutureTask task = new FutureTask(Callables.returning(null)); sched.start(mockMonkey, task); // make sure that the task gets completed within 100ms task.get(100L, TimeUnit.MILLISECONDS); sched.stop(mockMonkey); // create an event 5 min ago Calendar cal = Calendar.getInstance(); cal.add(Calendar.MINUTE, -5); BasicRecorderEvent evt = new BasicRecorderEvent( Enums.MONKEY, EventEnums.EVENT, "region", "test-id", cal.getTime().getTime()); context.recorder().recordEvent(evt); // this time when it runs it will not run immediately since it should be scheduled for 55m from now. task = new FutureTask(Callables.returning(null)); sched.start(mockMonkey, task); try { task.get(100, TimeUnit.MILLISECONDS); Assert.fail("The task shouldn't have been completed in 100ms"); } catch (TimeoutException e) { // NOPMD - This is an expected exception } sched.stop(mockMonkey); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/calendar/TestBavarianCalendar.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic.calendar; import com.netflix.simianarmy.basic.BasicConfiguration; import com.netflix.simianarmy.basic.calendars.BavarianCalendar; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.util.Calendar; import java.util.Properties; // CHECKSTYLE IGNORE MagicNumberCheck public class TestBavarianCalendar extends BavarianCalendar { private static final Properties PROPS = new Properties(); private static final BasicConfiguration CFG = new BasicConfiguration(PROPS); public TestBavarianCalendar() { super(CFG); } private Calendar now = super.now(); @Override public Calendar now() { return (Calendar) now.clone(); } private void setNow(Calendar now) { this.now = now; } @DataProvider public Object[][] easterDataProvider() { return new Object[][] { {1996, Calendar.APRIL, 7}, {1997, Calendar.MARCH, 30}, {1998, Calendar.APRIL, 12}, {1999, Calendar.APRIL, 4}, {2000, Calendar.APRIL, 23}, {2001, Calendar.APRIL, 15}, {2002, Calendar.MARCH, 31}, {2003, Calendar.APRIL, 20}, {2004, Calendar.APRIL, 11}, {2005, Calendar.MARCH, 27}, {2006, Calendar.APRIL, 16}, {2007, Calendar.APRIL, 8}, {2008, Calendar.MARCH, 23}, {2009, Calendar.APRIL, 12}, {2010, Calendar.APRIL, 4}, {2011, Calendar.APRIL, 24}, {2012, Calendar.APRIL, 8}, {2013, Calendar.MARCH, 31}, {2014, Calendar.APRIL, 20}, {2015, Calendar.APRIL, 5}, {2016, Calendar.MARCH, 27}, {2017, Calendar.APRIL, 16}, {2018, Calendar.APRIL, 1}, {2019, Calendar.APRIL, 21}, {2020, Calendar.APRIL, 12}, {2021, Calendar.APRIL, 4}, {2022, Calendar.APRIL, 17}, {2023, Calendar.APRIL, 9}, {2024, Calendar.MARCH, 31}, {2025, Calendar.APRIL, 20}, {2026, Calendar.APRIL, 5}, {2027, Calendar.MARCH, 28}, {2028, Calendar.APRIL, 16}, {2029, Calendar.APRIL, 1}, {2030, Calendar.APRIL, 21}, {2031, Calendar.APRIL, 13}, {2032, Calendar.MARCH, 28}, {2033, Calendar.APRIL, 17}, {2034, Calendar.APRIL, 9}, {2035, Calendar.MARCH, 25}, {2036, Calendar.APRIL, 13}, }; } @Test(dataProvider = "easterDataProvider") public void testEaster(int year, int month, int dayOfMonth) { Assert.assertEquals(dayOfYear(year, month, dayOfMonth), westernEasterDayOfYear(year)); } @DataProvider public Object[][] holidayDataProvider() { return new Object[][] { {2016, Calendar.JANUARY, 1}, // new year {2016, Calendar.JANUARY, 6}, // epiphanie {2016, Calendar.MARCH, 25}, // good friday {2016, Calendar.MARCH, 28}, // easter monday {2016, Calendar.MAY, 1}, // labor day {2016, Calendar.MAY, 5}, // ascension day {2016, Calendar.MAY, 6}, // friday after ascension day {2016, Calendar.MAY, 16}, // whit monday {2016, Calendar.MAY, 26}, // corpus christi {2016, Calendar.AUGUST, 15}, // assumption day {2016, Calendar.OCTOBER, 3}, // german unity day {2016, Calendar.DECEMBER, 24}, // christmas holidays {2016, Calendar.DECEMBER, 25}, {2016, Calendar.DECEMBER, 26}, {2016, Calendar.DECEMBER, 27}, {2016, Calendar.DECEMBER, 28}, {2016, Calendar.DECEMBER, 29}, {2016, Calendar.DECEMBER, 30}, {2016, Calendar.DECEMBER, 31}, // now, "bridge days" {2015, Calendar.JANUARY, 2}, // friday after new year {2015, Calendar.JANUARY, 5}, // monday before epiphanie {2011, Calendar.JANUARY, 7}, // friday after epiphanie {2012, Calendar.APRIL, 30}, // monday before labor day {2014, Calendar.MAY, 2}, // friday after labor day {2006, Calendar.AUGUST, 14}, // monday before assumption day {2013, Calendar.AUGUST, 16}, // friday after assumption day {2006, Calendar.OCTOBER, 2}, // monday before german unity day {2013, Calendar.OCTOBER, 4}, // friday after german unity day {2011, Calendar.OCTOBER, 31}, // monday before all saints {2012, Calendar.NOVEMBER, 2}, // friday after all saints {2013, Calendar.DECEMBER, 23} // monday before christas eve }; } @Test(dataProvider = "holidayDataProvider") public void testHolidays(int year, int month, int dayOfMonth) { Calendar test = Calendar.getInstance(); test.set(Calendar.YEAR, year); test.set(Calendar.MONTH, month); test.set(Calendar.DAY_OF_MONTH, dayOfMonth); test.set(Calendar.HOUR_OF_DAY, 10); setNow(test); Assert.assertTrue(isHoliday(test), test.getTime().toString() + " is a holiday?"); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/chaos/TestBasicChaosEmailNotifier.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic.chaos; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import java.util.Properties; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient; import com.netflix.simianarmy.GroupType; import com.netflix.simianarmy.basic.BasicConfiguration; import com.netflix.simianarmy.chaos.TestChaosMonkeyContext.TestInstanceGroup; public class TestBasicChaosEmailNotifier { private final AmazonSimpleEmailServiceClient sesClient = new AmazonSimpleEmailServiceClient(); private BasicChaosEmailNotifier basicChaosEmailNotifier; private Properties properties; private enum GroupTypes implements GroupType { TYPE_A }; private String name = "name0"; private String region = "reg1"; private String to = "foo@bar.com"; private String instanceId = "i-12345678901234567"; private String subjectPrefix = "Subject Prefix - "; private String subjectSuffix = " - Subject Suffix "; private String bodyPrefix = "Body Prefix - "; private String bodySuffix = " - Body Suffix"; private final TestInstanceGroup testInstanceGroup = new TestInstanceGroup(GroupTypes.TYPE_A, name, region, "0:" + instanceId); private String defaultBody = "Instance " + instanceId + " of " + GroupTypes.TYPE_A + " " + name + " is being terminated by Chaos monkey."; private String defaultSubject = "Chaos Monkey Termination Notification for " + to; @BeforeMethod public void beforeMethod() { properties = new Properties(); } @Test public void testInvalidEmailAddresses() { String[] invalidEmails = new String[] { "username", "username@.com.my", "username123@example.a", "username123@.com", "username123@.com.com", "username()*@example.com", "username@%*.com"}; basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); for (String emailAddress : invalidEmails) { Assert.assertFalse(basicChaosEmailNotifier.isValidEmail(emailAddress)); } } @Test public void testValidEmailAddresses() { String[] validEmails = new String[] { "username-100@example.com", "name.surname+ml-info@example.com", "username.100@example.com", "username111@example.com", "username-100@username.net", "username.100@example.com.au", "username@1.com", "username@example.com", "username+100@example.com", "username-100@example-test.com" }; basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); for (String emailAddress : validEmails) { Assert.assertTrue(basicChaosEmailNotifier.isValidEmail(emailAddress)); } } @Test public void testbuildEmailSubject() { basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); String subject = basicChaosEmailNotifier.buildEmailSubject(to); Assert.assertEquals(subject, defaultSubject); } @Test public void testbuildEmailSubjectWithSubjectPrefix() { properties.setProperty("simianarmy.chaos.notification.subject.prefix", subjectPrefix); basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); String subject = basicChaosEmailNotifier.buildEmailSubject(to); Assert.assertEquals(subject, subjectPrefix + defaultSubject); } @Test public void testbuildEmailSubjectWithSubjectSuffix() { properties.setProperty("simianarmy.chaos.notification.subject.suffix", subjectSuffix); basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); String subject = basicChaosEmailNotifier.buildEmailSubject(to); Assert.assertEquals(subject, defaultSubject + subjectSuffix); } @Test public void testbuildEmailSubjectWithSubjectPrefixSuffix() { properties.setProperty("simianarmy.chaos.notification.subject.prefix", subjectPrefix); properties.setProperty("simianarmy.chaos.notification.subject.suffix", subjectSuffix); basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); String subject = basicChaosEmailNotifier.buildEmailSubject(to); Assert.assertEquals(subject, subjectPrefix + defaultSubject + subjectSuffix); } @Test public void testbuildEmailBody() { basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); String subject = basicChaosEmailNotifier.buildEmailBody(testInstanceGroup, instanceId, null); Assert.assertEquals(subject, defaultBody); } @Test public void testbuildEmailBodyPrefix() { properties.setProperty("simianarmy.chaos.notification.body.prefix", bodyPrefix); basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); String subject = basicChaosEmailNotifier.buildEmailBody(testInstanceGroup, instanceId, null); Assert.assertEquals(subject, bodyPrefix + defaultBody); } @Test public void testbuildEmailBodySuffix() { properties.setProperty("simianarmy.chaos.notification.body.suffix", bodySuffix); basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); String subject = basicChaosEmailNotifier.buildEmailBody(testInstanceGroup, instanceId, null); Assert.assertEquals(subject, defaultBody + bodySuffix); } @Test public void testbuildEmailBodyPrefixSuffix() { properties.setProperty("simianarmy.chaos.notification.body.prefix", bodyPrefix); properties.setProperty("simianarmy.chaos.notification.body.suffix", bodySuffix); basicChaosEmailNotifier = new BasicChaosEmailNotifier(new BasicConfiguration(properties), sesClient, null); String subject = basicChaosEmailNotifier.buildEmailBody(testInstanceGroup, instanceId, null); Assert.assertEquals(subject, bodyPrefix + defaultBody + bodySuffix); } @Test public void testBuildAndSendEmail() { properties.setProperty("simianarmy.chaos.notification.sourceEmail", to); BasicChaosEmailNotifier spyBasicChaosEmailNotifier = spy(new BasicChaosEmailNotifier(new BasicConfiguration( properties), sesClient, null)); doNothing().when(spyBasicChaosEmailNotifier).sendEmail(to, defaultSubject, defaultBody); spyBasicChaosEmailNotifier.buildAndSendEmail(to, testInstanceGroup, instanceId, null); verify(spyBasicChaosEmailNotifier).sendEmail(to, defaultSubject, defaultBody); } @Test public void testBuildAndSendEmailSubjectIsBody() { properties.setProperty("simianarmy.chaos.notification.subject.isBody", "true"); properties.setProperty("simianarmy.chaos.notification.sourceEmail", to); BasicChaosEmailNotifier spyBasicChaosEmailNotifier = spy(new BasicChaosEmailNotifier(new BasicConfiguration( properties), sesClient, null)); doNothing().when(spyBasicChaosEmailNotifier).sendEmail(to, defaultBody, defaultBody); spyBasicChaosEmailNotifier.buildAndSendEmail(to, testInstanceGroup, instanceId, null); verify(spyBasicChaosEmailNotifier).sendEmail(to, defaultBody, defaultBody); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/chaos/TestBasicChaosInstanceSelector.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic.chaos; import java.util.*; import com.amazonaws.services.autoscaling.model.TagDescription; import com.netflix.simianarmy.GroupType; import com.netflix.simianarmy.chaos.ChaosInstanceSelector; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import org.testng.annotations.Test; import org.testng.annotations.DataProvider; import org.testng.Assert; import org.slf4j.Logger; import static org.slf4j.helpers.NOPLogger.NOP_LOGGER; // CHECKSTYLE IGNORE MagicNumberCheck public class TestBasicChaosInstanceSelector { private ChaosInstanceSelector selector = new BasicChaosInstanceSelector() { // turn off selector logger for this test since we call it ~1M times protected Logger logger() { return NOP_LOGGER; } }; public enum Types implements GroupType { TEST } private InstanceGroup group = new InstanceGroup() { public GroupType type() { return Types.TEST; } public String name() { return "TestGroup"; } public String region() { return "region"; } public List tags() { return Collections.emptyList(); } public List instances() { return Arrays.asList("i-123456789012345670", "i-123456789012345671", "i-123456789012345672", "i-123456789012345673", "i-123456789012345674", "i-123456789012345675", "i-123456789012345676", "i-123456789012345677", "i-123456789012345678", "i-123456789012345679"); } public void addInstance(String ignored) { } @Override public InstanceGroup copyAs(String name) { return this; } }; @Test public void testSelect() { Assert.assertTrue(selector.select(group, 0).isEmpty(), "select disabled group is always null"); Assert.assertTrue(selector.select(group, 0.0).isEmpty(), "select disabled group is always null"); int selected = 0; for (int i = 0; i < 100; i++) { selected += selector.select(group, 1.0).size(); } Assert.assertEquals(selected, 100, "1.0 probability always selects an instance"); } @DataProvider public Object[][] evenSelectionDataProvider() { return new Object[][] {{1.0}, {0.9}, {0.8}, {0.7}, {0.6}, {0.5}, {0.4}, {0.3}, {0.2}, {0.1} }; } static final int RUNS = 1000000; @Test(dataProvider = "evenSelectionDataProvider") public void testEvenSelections(double probability) { Map selectMap = new HashMap(); for (int i = 0; i < RUNS; i++) { Collection instances = selector.select(group, probability); for (String inst : instances) { if (selectMap.containsKey(inst)) { selectMap.put(inst, selectMap.get(inst) + 1); } else { selectMap.put(inst, 1); } } } Assert.assertEquals(selectMap.size(), group.instances().size(), "verify we selected all instances"); // allow for 4% variation over all the selection runs int avg = Double.valueOf((RUNS / (double) group.instances().size()) * probability).intValue(); int max = Double.valueOf(avg + (avg * 0.04)).intValue(); int min = Double.valueOf(avg - (avg * 0.04)).intValue(); for (Map.Entry pair : selectMap.entrySet()) { Assert.assertTrue(pair.getValue() > min && pair.getValue() < max, pair.getKey() + " selected " + avg + " +- 4% times for prob: " + probability + " [got: " + pair.getValue() + "]"); } } @Test public void testSelectWithProbMoreThanOne() { // The number of selected instances should always be p when the prob is an integer. for (int p = 0; p <= group.instances().size(); p++) { Assert.assertEquals(selector.select(group, p).size(), p); } // When the prob is bigger than the size of the group, we get the whole group. for (int p = group.instances().size(); p <= group.instances().size() * 2; p++) { Assert.assertEquals(selector.select(group, p).size(), group.instances().size()); } } @Test public void testSelectWithProbMoreThanOneWithFraction() { // The number of selected instances can be p or p+1, depending on whether the fraction part // can get a instance selected. for (int p = 0; p <= group.instances().size(); p++) { Collection selected = selector.select(group, p + 0.5); Assert.assertTrue(selected.size() >= p && selected.size() <= p + 1); } // When the prob is bigger than the size of the group, we get the whole group. for (int p = group.instances().size(); p <= group.instances().size() * 2; p++) { Collection selected = selector.select(group, p + 0.5); Assert.assertEquals(selected.size(), group.instances().size()); } } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/chaos/TestBasicChaosMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic.chaos; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import javax.ws.rs.core.Response; import com.netflix.simianarmy.GroupType; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.Monkey; import com.netflix.simianarmy.MonkeyScheduler; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import com.netflix.simianarmy.chaos.ChaosMonkey; import com.netflix.simianarmy.chaos.TestChaosMonkeyContext; import com.netflix.simianarmy.resources.chaos.ChaosMonkeyResource; import com.amazonaws.services.autoscaling.model.TagDescription; // CHECKSTYLE IGNORE MagicNumberCheck public class TestBasicChaosMonkey { private enum GroupTypes implements GroupType { TYPE_A, TYPE_B }; @Test public void testDisabled() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("disabled.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 0, "no groups selected on"); Assert.assertEquals(terminated.size(), 0, "nothing terminated"); } @Test public void testEnabledA() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("enabledA.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 2); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(1).name(), "name1"); Assert.assertEquals(terminated.size(), 0, "nothing terminated"); } @Test public void testUnleashedEnabledA() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("unleashedEnabledA.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 2); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(1).name(), "name1"); Assert.assertEquals(terminated.size(), 2); Assert.assertEquals(terminated.get(0), "0:i-123456789012345670"); Assert.assertEquals(terminated.get(1), "1:i-123456789012345671"); } @Test public void testEnabledB() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("enabledB.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 2); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(0).name(), "name2"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(1).name(), "name3"); Assert.assertEquals(terminated.size(), 0, "nothing terminated"); } @Test public void testUnleashedEnabledB() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("unleashedEnabledB.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 2); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(0).name(), "name2"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(1).name(), "name3"); Assert.assertEquals(terminated.size(), 2); Assert.assertEquals(terminated.get(0), "2:i-123456789012345672"); Assert.assertEquals(terminated.get(1), "3:i-123456789012345673"); } @Test public void testEnabledAwithout1() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("enabledAwithout1.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 1); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(terminated.size(), 1); Assert.assertEquals(terminated.get(0), "0:i-123456789012345670"); } @Test public void testEnabledAwith0() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("enabledAwith0.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 1); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(terminated.size(), 1); Assert.assertEquals(terminated.get(0), "0:i-123456789012345670"); } @Test public void testAll() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("all.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 4); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(1).name(), "name1"); Assert.assertEquals(selectedOn.get(2).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(2).name(), "name2"); Assert.assertEquals(selectedOn.get(3).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(3).name(), "name3"); Assert.assertEquals(terminated.size(), 4); Assert.assertEquals(terminated.get(0), "0:i-123456789012345670"); Assert.assertEquals(terminated.get(1), "1:i-123456789012345671"); Assert.assertEquals(terminated.get(2), "2:i-123456789012345672"); Assert.assertEquals(terminated.get(3), "3:i-123456789012345673"); } @Test public void testNoProbability() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("noProbability.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 4); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(1).name(), "name1"); Assert.assertEquals(selectedOn.get(2).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(2).name(), "name2"); Assert.assertEquals(selectedOn.get(3).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(3).name(), "name3"); Assert.assertEquals(terminated.size(), 0); } @Test public void testFullProbability() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("fullProbability.properties") { @Override public MonkeyScheduler scheduler() { return new MonkeyScheduler() { @Override public int frequency() { return 1; } @Override public TimeUnit frequencyUnit() { return TimeUnit.DAYS; } @Override public void start(Monkey monkey, Runnable run) { Assert.assertEquals(monkey.type().name(), monkey.type().name(), "starting monkey"); run.run(); } @Override public void stop(Monkey monkey) { Assert.assertEquals(monkey.type().name(), monkey.type().name(), "stopping monkey"); } }; } }; ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 4); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(1).name(), "name1"); Assert.assertEquals(selectedOn.get(2).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(2).name(), "name2"); Assert.assertEquals(selectedOn.get(3).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(3).name(), "name3"); Assert.assertEquals(terminated.size(), 4); } @Test public void testNoProbabilityByName() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("noProbabilityByName.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); List selectedOn = ctx.selectedOn(); List terminated = ctx.terminated(); Assert.assertEquals(selectedOn.size(), 4); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(1).name(), "name1"); Assert.assertEquals(selectedOn.get(2).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(2).name(), "name2"); Assert.assertEquals(selectedOn.get(3).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_B); Assert.assertEquals(selectedOn.get(3).name(), "name3"); Assert.assertEquals(terminated.size(), 0); } @Test public void testMaxTerminationCountPerDayAsZero() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("terminationPerDayAsZero.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 0); Assert.assertEquals(ctx.terminated().size(), 0); } @Test public void testMaxTerminationCountPerDayAsOne() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("terminationPerDayAsOne.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); // Run the chaos the second time will NOT trigger another termination chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); } @Test public void testMaxTerminationCountPerDayAsBiggerThanOne() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("terminationPerDayAsBiggerThanOne.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); // Run the chaos the second time will trigger another termination chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 2); Assert.assertEquals(ctx.terminated().size(), 2); } @Test public void testMaxTerminationCountPerDayAsSmallerThanOne() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("terminationPerDayAsSmallerThanOne.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); // Run the chaos the second time will NOT trigger another termination chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); } @Test public void testMaxTerminationCountPerDayAsNegative() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("terminationPerDayAsNegative.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 0); Assert.assertEquals(ctx.terminated().size(), 0); } @Test public void testMaxTerminationCountPerDayAsVerySmall() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("terminationPerDayAsVerySmall.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 0); Assert.assertEquals(ctx.terminated().size(), 0); } @Test public void testMaxTerminationCountPerDayGroupLevel() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("terminationPerDayGroupLevel.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); for (int i = 1; i <= 3; i++) { chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), i); Assert.assertEquals(ctx.terminated().size(), i); } // Run the chaos the second time will NOT trigger another termination chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 3); Assert.assertEquals(ctx.terminated().size(), 3); } @Test public void testGetValueFromCfgWithDefault() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("propertiesWithDefaults.properties"); BasicChaosMonkey chaos = new BasicChaosMonkey(ctx); // named 1 has actual values in config InstanceGroup named1 = new BasicInstanceGroup("named1", GroupTypes.TYPE_A, "test-dev-1", Collections.emptyList()); // named 2 doesn't have values but it's group has values InstanceGroup named2 = new BasicInstanceGroup("named2", GroupTypes.TYPE_A, "test-dev-1", Collections.emptyList()); // named 3 doesn't have values and it's group doesn't have values InstanceGroup named3 = new BasicInstanceGroup("named3", GroupTypes.TYPE_B, "test-dev-1", Collections.emptyList()); Assert.assertEquals(chaos.getBoolFromCfgOrDefault(named1, "enabled", true), false); Assert.assertEquals(chaos.getNumFromCfgOrDefault(named1, "probability", 3.0), 1.1); Assert.assertEquals(chaos.getNumFromCfgOrDefault(named1, "maxTerminationsPerDay", 4.0), 2.1); Assert.assertEquals(chaos.getBoolFromCfgOrDefault(named2, "enabled", true), true); Assert.assertEquals(chaos.getNumFromCfgOrDefault(named2, "probability", 3.0), 1.0); Assert.assertEquals(chaos.getNumFromCfgOrDefault(named2, "maxTerminationsPerDay", 4.0), 2.0); Assert.assertEquals(chaos.getBoolFromCfgOrDefault(named3, "enabled", true), true); Assert.assertEquals(chaos.getNumFromCfgOrDefault(named3, "probability", 3.0), 3.0); Assert.assertEquals(chaos.getNumFromCfgOrDefault(named3, "maxTerminationsPerDay", 4.0), 4.0); } @Test public void testMandatoryTerminationDisabled() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("mandatoryTerminationDisabled.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 0); } @Test public void testMandatoryTerminationNotDefined() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("mandatoryTerminationNotDefined.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 0); } @Test public void testMandatoryTerminationNoOptInTime() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("mandatoryTerminationNoOptInTime.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 0); } @Test public void testMandatoryTerminationInsideWindow() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("mandatoryTerminationInsideWindow.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); // The last opt-in time is within the window, so no mandatory termination is triggered Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 0); } @Test public void testMandatoryTerminationOutsideWindow() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("mandatoryTerminationOutsideWindow.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); // There was no termination in the last window, so one mandatory termination is triggered Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); } @Test public void testMandatoryTerminationOutsideWindowWithPreviousTermination() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("mandatoryTerminationOutsideWindow.properties"); terminateOnDemand(ctx, "TYPE_C", "name4"); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); // There was termination in the last window, so no mandatory termination is triggered Assert.assertEquals(ctx.selectedOn().size(), 2); Assert.assertEquals(ctx.terminated().size(), 1); } @Test public void testMandatoryTerminationInsideWindowWithPreviousTermination() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("mandatoryTerminationInsideWindow.properties"); terminateOnDemand(ctx, "TYPE_C", "name4"); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); // There was termination in the last window, so no mandatory termination is triggered Assert.assertEquals(ctx.selectedOn().size(), 2); Assert.assertEquals(ctx.terminated().size(), 1); } @Test public void testNotificationEnabled() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("notificationEnabled.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 4); Assert.assertEquals(ctx.terminated().size(), 4); // Notification is enabled only for 2 terminations. Assert.assertEquals(ctx.getNotified(), 2); } @Test public void testGlobalNotificationEnabled() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("globalNotificationEnabled.properties"); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 4); Assert.assertEquals(ctx.terminated().size(), 4); Assert.assertEquals(ctx.getNotified(), 1); Assert.assertEquals(ctx.getGloballyNotified(), 4); } private void terminateOnDemand(TestChaosMonkeyContext ctx, String groupType, String groupName) { String input = String.format("{\"eventType\":\"CHAOS_TERMINATION\",\"groupType\":\"%s\",\"groupName\":\"%s\"}", groupType, groupName); int currentSelectedOn = ctx.selectedOn().size(); int currentTerminated = ctx.terminated().size(); ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.OK); Assert.assertEquals(ctx.selectedOn().size(), currentSelectedOn + 1); Assert.assertEquals(ctx.terminated().size(), currentTerminated + 1); } private void validateAddEventResult(ChaosMonkeyResource resource, String input, Response.Status responseStatus) { try { Response resp = resource.addEvent(input); Assert.assertEquals(resp.getStatus(), responseStatus.getStatusCode()); } catch (Exception e) { Assert.fail("addEvent throws exception"); } } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/chaos/TestCloudFormationChaosMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.basic.chaos; import com.amazonaws.services.autoscaling.model.TagDescription; import org.testng.Assert; import org.testng.annotations.Test; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertFalse; import com.netflix.simianarmy.chaos.TestChaosMonkeyContext; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import java.util.Collections; public class TestCloudFormationChaosMonkey { public static final long EXPECTED_MILLISECONDS = 2000; @Test public void testIsGroupEnabled() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("cloudformation.properties"); CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx); InstanceGroup group1 = new BasicInstanceGroup("new-group-TestGroup1-XCFNFNFNF", TestChaosMonkeyContext.CrawlerTypes.TYPE_D, "region", Collections.emptyList()); InstanceGroup group2 = new BasicInstanceGroup("new-group-TestGroup2-XCFNGHFNF", TestChaosMonkeyContext.CrawlerTypes.TYPE_D, "region", Collections.emptyList()); assertTrue(chaos.isGroupEnabled(group1)); assertFalse(chaos.isGroupEnabled(group2)); } @Test public void testIsMaxTerminationCountExceeded() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("cloudformation.properties"); CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx); InstanceGroup group1 = new BasicInstanceGroup("new-group-TestGroup1-XCFNFNFNF", TestChaosMonkeyContext.CrawlerTypes.TYPE_D, "region", Collections.emptyList()); assertFalse(chaos.isMaxTerminationCountExceeded(group1)); } @Test public void testGetEffectiveProbability() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("cloudformation.properties"); CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx); InstanceGroup group1 = new BasicInstanceGroup("new-group-TestGroup1-XCFNFNFNF", TestChaosMonkeyContext.CrawlerTypes.TYPE_D, "region", Collections.emptyList()); assertEquals(1.0, chaos.getEffectiveProbability(group1)); } @Test public void testNoSuffixInstanceGroup() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("disabled.properties"); CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx); InstanceGroup group = new BasicInstanceGroup("new-group-TestGroup-XCFNFNFNF", TestChaosMonkeyContext.CrawlerTypes.TYPE_D, "region", Collections.emptyList()); InstanceGroup newGroup = chaos.noSuffixInstanceGroup(group); assertEquals(newGroup.name(), "new-group-TestGroup"); } @Test public void testGetLastOptInMilliseconds() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("cloudformation.properties"); CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx); InstanceGroup group = new BasicInstanceGroup("new-group-TestGroup1-XCFNFNFNF", TestChaosMonkeyContext.CrawlerTypes.TYPE_D, "region", Collections.emptyList()); assertEquals(chaos.getLastOptInMilliseconds(group), EXPECTED_MILLISECONDS); } @Test public void testCloudFormationChaosMonkeyIntegration() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("cloudformation.properties"); CloudFormationChaosMonkey chaos = new CloudFormationChaosMonkey(ctx); chaos.start(); chaos.stop(); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); Assert.assertEquals(ctx.getNotified(), 1); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/basic/janitor/TestBasicJanitorRuleEngine.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.basic.janitor; import com.netflix.simianarmy.Resource; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.janitor.Rule; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; import java.util.Date; public class TestBasicJanitorRuleEngine { @Test public void testEmptyRuleSet() { Resource resource = new AWSResource().withId("id"); BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine(); Assert.assertTrue(engine.isValid(resource)); } @Test public void testAllValid() { Resource resource = new AWSResource().withId("id"); BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine() .addRule(new AlwaysValidRule()) .addRule(new AlwaysValidRule()) .addRule(new AlwaysValidRule()); Assert.assertTrue(engine.isValid(resource)); } @Test public void testMixed() { Resource resource = new AWSResource().withId("id"); DateTime now = DateTime.now(); BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine() .addRule(new AlwaysValidRule()) .addRule(new AlwaysInvalidRule(now, 1)) .addRule(new AlwaysValidRule()); Assert.assertFalse(engine.isValid(resource)); } @Test public void testIsValidWithNearestTerminationTime() { int[][] permutaions = {{1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 1}, {3, 1, 2}, {3, 2, 1}}; for (int[] perm : permutaions) { Resource resource = new AWSResource().withId("id"); DateTime now = DateTime.now(); BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine() .addRule(new AlwaysInvalidRule(now, perm[0])) .addRule(new AlwaysInvalidRule(now, perm[1])) .addRule(new AlwaysInvalidRule(now, perm[2])); Assert.assertFalse(engine.isValid(resource)); Assert.assertEquals( resource.getExpectedTerminationTime().getTime(), now.plusDays(1).getMillis()); Assert.assertEquals(resource.getTerminationReason(), "1"); } } @Test void testWithExclusionRuleMatch1() { Resource resource = new AWSResource().withId("id"); DateTime now = DateTime.now(); BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine() .addExclusionRule(new AlwaysValidRule()) .addRule(new AlwaysInvalidRule(now, 1)); Assert.assertTrue(engine.isValid(resource)); } @Test void testWithExclusionRuleMatch2() { Resource resource = new AWSResource().withId("id"); DateTime now = DateTime.now(); BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine() .addExclusionRule(new AlwaysValidRule()) .addRule(new AlwaysValidRule()); Assert.assertTrue(engine.isValid(resource)); } @Test void testWithExclusionRuleNotMatch1() { Resource resource = new AWSResource().withId("id"); DateTime now = DateTime.now(); BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine() .addExclusionRule(new AlwaysInvalidRule(now, 1)) .addRule(new AlwaysInvalidRule(now, 1)); Assert.assertFalse(engine.isValid(resource)); } @Test void testWithExclusionRuleNotMatch2() { Resource resource = new AWSResource().withId("id"); DateTime now = DateTime.now(); BasicJanitorRuleEngine engine = new BasicJanitorRuleEngine() .addExclusionRule(new AlwaysInvalidRule(now, 1)) .addRule(new AlwaysValidRule()); Assert.assertTrue(engine.isValid(resource)); } } class AlwaysValidRule implements Rule { @Override public boolean isValid(Resource resource) { return true; } } class AlwaysInvalidRule implements Rule { private final int retentionDays; private final DateTime now; public AlwaysInvalidRule(DateTime now, int retentionDays) { this.retentionDays = retentionDays; this.now = now; } @Override public boolean isValid(Resource resource) { resource.setExpectedTerminationTime( new Date(now.plusDays(retentionDays).getMillis())); resource.setTerminationReason(String.valueOf(retentionDays)); return false; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/chaos/TestChaosMonkeyArmy.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.chaos; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Properties; import org.testng.Assert; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; import com.google.common.base.Charsets; import com.google.common.io.Files; import com.netflix.simianarmy.basic.chaos.BasicChaosMonkey; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import com.netflix.simianarmy.chaos.TestChaosMonkeyContext.Notification; import com.netflix.simianarmy.chaos.TestChaosMonkeyContext.SshAction; // CHECKSTYLE IGNORE MagicNumberCheck public class TestChaosMonkeyArmy { private File sshKey; @BeforeTest public void createSshKey() throws IOException { sshKey = File.createTempFile("tmp", "key"); Files.write("fakekey", sshKey, Charsets.UTF_8); sshKey.deleteOnExit(); } private TestChaosMonkeyContext runChaosMonkey(String key) { return runChaosMonkey(key, true); } private TestChaosMonkeyContext runChaosMonkey(String key, boolean burnMoney) { Properties properties = new Properties(); properties.setProperty("simianarmy.chaos.enabled", "true"); properties.setProperty("simianarmy.chaos.leashed", "false"); properties.setProperty("simianarmy.chaos.TYPE_A.enabled", "true"); properties.setProperty("simianarmy.chaos.notification.global.enabled", "true"); properties.setProperty("simianarmy.chaos.burnmoney", Boolean.toString(burnMoney)); properties.setProperty("simianarmy.chaos.shutdowninstance.enabled", "false"); properties.setProperty("simianarmy.chaos." + key.toLowerCase() + ".enabled", "true"); properties.setProperty("simianarmy.chaos.ssh.key", sshKey.getAbsolutePath()); TestChaosMonkeyContext ctx = new TestChaosMonkeyContext(properties); ChaosMonkey chaos = new BasicChaosMonkey(ctx); chaos.start(); chaos.stop(); return ctx; } private void checkSelected(TestChaosMonkeyContext ctx) { List selectedOn = ctx.selectedOn(); Assert.assertEquals(selectedOn.size(), 2); Assert.assertEquals(selectedOn.get(0).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(0).name(), "name0"); Assert.assertEquals(selectedOn.get(1).type(), TestChaosMonkeyContext.CrawlerTypes.TYPE_A); Assert.assertEquals(selectedOn.get(1).name(), "name1"); } private void checkNotifications(TestChaosMonkeyContext ctx, String key) { List notifications = ctx.getGloballyNotifiedList(); Assert.assertEquals(notifications.size(), 2); Assert.assertEquals(notifications.get(0).getInstance(), "0:i-123456789012345670"); Assert.assertEquals(notifications.get(0).getChaosType().getKey(), key); Assert.assertEquals(notifications.get(1).getInstance(), "1:i-123456789012345671"); Assert.assertEquals(notifications.get(1).getChaosType().getKey(), key); } private void checkSshActions(TestChaosMonkeyContext ctx, String key) { List sshActions = ctx.getSshActions(); Assert.assertEquals(sshActions.size(), 4); Assert.assertEquals(sshActions.get(0).getMethod(), "put"); Assert.assertEquals(sshActions.get(0).getInstanceId(), "0:i-123456789012345670"); // We require that each script include the name of the chaos type // This makes testing easier, and also means the scripts show where they came from Assert.assertTrue(sshActions.get(0).getContents().toLowerCase().contains(key.toLowerCase())); Assert.assertEquals(sshActions.get(1).getMethod(), "exec"); Assert.assertEquals(sshActions.get(1).getInstanceId(), "0:i-123456789012345670"); Assert.assertEquals(sshActions.get(2).getMethod(), "put"); Assert.assertEquals(sshActions.get(2).getInstanceId(), "1:i-123456789012345671"); Assert.assertTrue(sshActions.get(2).getContents().contains(key)); Assert.assertEquals(sshActions.get(3).getMethod(), "exec"); Assert.assertEquals(sshActions.get(3).getInstanceId(), "1:i-123456789012345671"); } @Test public void testShutdownInstance() { String key = "ShutdownInstance"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); List terminated = ctx.terminated(); Assert.assertEquals(terminated.size(), 2); Assert.assertEquals(terminated.get(0), "0:i-123456789012345670"); Assert.assertEquals(terminated.get(1), "1:i-123456789012345671"); } @Test public void testBlockAllNetworkTraffic() { String key = "BlockAllNetworkTraffic"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); List cloudActions = ctx.getCloudActions(); Assert.assertEquals(cloudActions.size(), 3); Assert.assertEquals(cloudActions.get(0), "createSecurityGroup:0:i-123456789012345670:blocked-network"); Assert.assertEquals(cloudActions.get(1), "setInstanceSecurityGroups:0:i-123456789012345670:sg-1"); Assert.assertEquals(cloudActions.get(2), "setInstanceSecurityGroups:1:i-123456789012345671:sg-1"); } @Test public void testDetachVolumes() { String key = "DetachVolumes"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); List cloudActions = ctx.getCloudActions(); Assert.assertEquals(cloudActions.size(), 4); Assert.assertEquals(cloudActions.get(0), "detach:0:i-123456789012345670:volume-1"); Assert.assertEquals(cloudActions.get(1), "detach:0:i-123456789012345670:volume-2"); Assert.assertEquals(cloudActions.get(2), "detach:1:i-123456789012345671:volume-1"); Assert.assertEquals(cloudActions.get(3), "detach:1:i-123456789012345671:volume-2"); } @Test public void testBurnCpu() { String key = "BurnCpu"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testBurnIo() { String key = "BurnIO"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testBurnIoWithoutBurnMoney() { String key = "BurnIO"; TestChaosMonkeyContext ctx = runChaosMonkey(key, false); checkSelected(ctx); List notifications = ctx.getGloballyNotifiedList(); Assert.assertEquals(notifications.size(), 0); List sshActions = ctx.getSshActions(); Assert.assertEquals(sshActions.size(), 0); } @Test public void testFillDisk() { String key = "FillDisk"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testFillDiskWithoutBurnMoney() { String key = "FillDisk"; TestChaosMonkeyContext ctx = runChaosMonkey(key, false); checkSelected(ctx); List notifications = ctx.getGloballyNotifiedList(); Assert.assertEquals(notifications.size(), 0); List sshActions = ctx.getSshActions(); Assert.assertEquals(sshActions.size(), 0); } @Test public void testFailDns() { String key = "FailDns"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testFailDynamoDb() { String key = "FailDynamoDb"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testFailEc2() { String key = "FailEc2"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testFailS3() { String key = "FailS3"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testKillProcess() { String key = "KillProcesses"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testNetworkCorruption() { String key = "NetworkCorruption"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testNetworkLatency() { String key = "NetworkLatency"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testNetworkLoss() { String key = "NetworkLoss"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } @Test public void testNullRoute() { String key = "NullRoute"; TestChaosMonkeyContext ctx = runChaosMonkey(key); checkSelected(ctx); checkNotifications(ctx, key); checkSshActions(ctx, key); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/chaos/TestChaosMonkeyContext.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.chaos; import com.amazonaws.services.autoscaling.model.TagDescription; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.netflix.simianarmy.CloudClient; import com.netflix.simianarmy.GroupType; import com.netflix.simianarmy.MonkeyConfiguration; import com.netflix.simianarmy.TestMonkeyContext; import com.netflix.simianarmy.basic.BasicConfiguration; import com.netflix.simianarmy.basic.chaos.BasicChaosInstanceSelector; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import org.jclouds.compute.ComputeService; import org.jclouds.compute.domain.ExecChannel; import org.jclouds.compute.domain.ExecResponse; import org.jclouds.domain.LoginCredentials; import org.jclouds.io.Payload; import org.jclouds.ssh.SshClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.InputStream; import java.util.*; public class TestChaosMonkeyContext extends TestMonkeyContext implements ChaosMonkey.Context { private static final Logger LOGGER = LoggerFactory.getLogger(TestChaosMonkeyContext.class); private final BasicConfiguration cfg; public TestChaosMonkeyContext() { this(new Properties()); } protected TestChaosMonkeyContext(Properties properties) { super(ChaosMonkey.Type.CHAOS); cfg = new BasicConfiguration(properties); } public TestChaosMonkeyContext(String propFile) { super(ChaosMonkey.Type.CHAOS); Properties props = new Properties(); try { InputStream is = TestChaosMonkeyContext.class.getResourceAsStream(propFile); try { props.load(is); } finally { is.close(); } } catch (Exception e) { LOGGER.error("Unable to load properties file " + propFile, e); } cfg = new BasicConfiguration(props); } @Override public MonkeyConfiguration configuration() { return cfg; } public static class TestInstanceGroup implements InstanceGroup { private final GroupType type; private final String name; private final String region; private final List instances = new ArrayList(); private final List tags = new ArrayList(); public TestInstanceGroup(GroupType type, String name, String region, String... instances) { this.type = type; this.name = name; this.region = region; for (String i : instances) { this.instances.add(i); } } @Override public List tags() { return tags; } @Override public GroupType type() { return type; } @Override public String name() { return name; } @Override public String region() { return region; } @Override public List instances() { return Collections.unmodifiableList(instances); } @Override public void addInstance(String ignored) { } public void deleteInstance(String id) { instances.remove(id); } @Override public InstanceGroup copyAs(String newName) { return new TestInstanceGroup(this.type, newName, this.region, instances().toString()); } } public enum CrawlerTypes implements GroupType { TYPE_A, TYPE_B, TYPE_C, TYPE_D }; @Override public ChaosCrawler chaosCrawler() { return new ChaosCrawler() { @Override public EnumSet groupTypes() { return EnumSet.allOf(CrawlerTypes.class); } @Override public List groups() { InstanceGroup gA0 = new TestInstanceGroup(CrawlerTypes.TYPE_A, "name0", "reg1", "0:i-123456789012345670"); InstanceGroup gA1 = new TestInstanceGroup(CrawlerTypes.TYPE_A, "name1", "reg1", "1:i-123456789012345671"); InstanceGroup gB2 = new TestInstanceGroup(CrawlerTypes.TYPE_B, "name2", "reg1", "2:i-123456789012345672"); InstanceGroup gB3 = new TestInstanceGroup(CrawlerTypes.TYPE_B, "name3", "reg1", "3:i-123456789012345673"); InstanceGroup gC1 = new TestInstanceGroup(CrawlerTypes.TYPE_C, "name4", "reg1", "3:i-123456789012345674", "3:i-123456789012345675"); InstanceGroup gC2 = new TestInstanceGroup(CrawlerTypes.TYPE_C, "name5", "reg1", "3:i-123456789012345676", "3:i-123456789012345677"); InstanceGroup gD0 = new TestInstanceGroup(CrawlerTypes.TYPE_D, "new-group-TestGroup1-XXXXXXXXX", "reg1", "3:i-123456789012345678", "3:i-123456789012345679"); return Arrays.asList(gA0, gA1, gB2, gB3, gC1, gC2, gD0); } @Override public List groups(String... names) { Map nameToGroup = new HashMap(); for (InstanceGroup ig : groups()) { nameToGroup.put(ig.name(), ig); } List list = new LinkedList(); for (String name : names) { InstanceGroup ig = nameToGroup.get(name); if (ig == null) { continue; } for (String instanceId : selected) { // Remove selected instances from crawler list TestInstanceGroup testIg = (TestInstanceGroup) ig; testIg.deleteInstance(instanceId); } list.add(ig); } return list; } }; } private final List selectedOn = new LinkedList(); public List selectedOn() { return selectedOn; } @Override public ChaosInstanceSelector chaosInstanceSelector() { return new BasicChaosInstanceSelector() { @Override public Collection select(InstanceGroup group, double probability) { selectedOn.add(group); Collection instances = super.select(group, probability); selected.addAll(instances); return instances; } }; } private final List terminated = new LinkedList(); private final List selected = Lists.newArrayList(); private final List cloudActions = Lists.newArrayList(); public List terminated() { return terminated; } private final Map securityGroupNames = Maps.newHashMap(); @Override public CloudClient cloudClient() { return new CloudClient() { @Override public void terminateInstance(String instanceId) { terminated.add(instanceId); } @Override public void createTagsForResources(Map keyValueMap, String... resourceIds) { } @Override public void deleteAutoScalingGroup(String asgName) { } @Override public void deleteVolume(String volumeId) { } @Override public void deleteSnapshot(String snapshotId) { } @Override public void deleteImage(String imageId) { } @Override public void deleteLaunchConfiguration(String launchConfigName) { } @Override public void deleteElasticLoadBalancer(String elbId) { } @Override public void deleteDNSRecord(String dnsname, String dnstype, String hostedzoneid) { } @Override public List listAttachedVolumes(String instanceId, boolean includeRoot) { List volumes = Lists.newArrayList(); if (includeRoot) { volumes.add("volume-0"); } volumes.add("volume-1"); volumes.add("volume-2"); return volumes; } @Override public void detachVolume(String instanceId, String volumeId, boolean force) { cloudActions.add("detach:" + instanceId + ":" + volumeId); } @Override public ComputeService getJcloudsComputeService() { throw new UnsupportedOperationException(); } @Override public String getJcloudsId(String instanceId) { throw new UnsupportedOperationException(); } @Override public SshClient connectSsh(String instanceId, LoginCredentials credentials) { return new MockSshClient(instanceId, credentials); } @Override public String findSecurityGroup(String instanceId, String groupName) { return securityGroupNames.get(groupName); } @Override public String createSecurityGroup(String instanceId, String groupName, String description) { String id = "sg-" + (securityGroupNames.size() + 1); securityGroupNames.put(groupName, id); cloudActions.add("createSecurityGroup:" + instanceId + ":" + groupName); return id; } @Override public boolean canChangeInstanceSecurityGroups(String instanceId) { return true; } @Override public void setInstanceSecurityGroups(String instanceId, List groupIds) { cloudActions.add("setInstanceSecurityGroups:" + instanceId + ":" + Joiner.on(',').join(groupIds)); } }; } private final List sshActions = Lists.newArrayList(); public static class SshAction { private String instanceId; private String method; private String path; private String contents; private String command; public String getInstanceId() { return instanceId; } public String getMethod() { return method; } public String getPath() { return path; } public String getContents() { return contents; } public String getCommand() { return command; } } private class MockSshClient implements SshClient { private final String instanceId; private final LoginCredentials credentials; public MockSshClient(String instanceId, LoginCredentials credentials) { this.instanceId = instanceId; this.credentials = credentials; } @Override public String getUsername() { return credentials.getUser(); } @Override public String getHostAddress() { throw new UnsupportedOperationException(); } @Override public void put(String path, Payload contents) { throw new UnsupportedOperationException(); } @Override public Payload get(String path) { throw new UnsupportedOperationException(); } @Override public ExecResponse exec(String command) { SshAction action = new SshAction(); action.method = "exec"; action.instanceId = instanceId; action.command = command; sshActions.add(action); String output = ""; String error = ""; int exitStatus = 0; return new ExecResponse(output, error, exitStatus); } @Override public ExecChannel execChannel(String command) { throw new UnsupportedOperationException(); } @Override public void connect() { } @Override public void disconnect() { } @Override public void put(String path, String contents) { SshAction action = new SshAction(); action.method = "put"; action.instanceId = instanceId; action.path = path; action.contents = contents; sshActions.add(action); } } private List groupNotified = Lists.newArrayList(); private List globallyNotified = Lists.newArrayList(); static class Notification { private final String instance; private final ChaosType chaosType; public Notification(String instance, ChaosType chaosType) { this.instance = instance; this.chaosType = chaosType; } public String getInstance() { return instance; } public ChaosType getChaosType() { return chaosType; } } @Override public ChaosEmailNotifier chaosEmailNotifier() { return new ChaosEmailNotifier(null) { @Override public String getSourceAddress(String to) { return "source@chaosMonkey.foo"; } @Override public String[] getCcAddresses(String to) { return new String[] {}; } @Override public String buildEmailSubject(String to) { return String.format("Testing Chaos termination notification for %s", to); } @Override public void sendTerminationNotification(InstanceGroup group, String instance, ChaosType chaosType) { groupNotified.add(new Notification(instance, chaosType)); } @Override public void sendTerminationGlobalNotification(InstanceGroup group, String instance, ChaosType chaosType) { globallyNotified.add(new Notification(instance, chaosType)); } }; } public int getNotified() { return groupNotified.size(); } public int getGloballyNotified() { return globallyNotified.size(); } public List getNotifiedList() { return groupNotified; } public List getGloballyNotifiedList() { return globallyNotified; } public List getSshActions() { return sshActions; } public List getCloudActions() { return cloudActions; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/client/aws/TestAWSClient.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.client.aws; import com.amazonaws.services.autoscaling.AmazonAutoScalingClient; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.DescribeAutoScalingGroupsRequest; import com.amazonaws.services.autoscaling.model.DescribeAutoScalingGroupsResult; import com.amazonaws.services.autoscaling.model.Instance; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import org.mockito.ArgumentCaptor; import org.testng.Assert; import org.testng.annotations.Test; import java.util.Arrays; import java.util.List; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class TestAWSClient extends AWSClient { public TestAWSClient() { super("us-east-1"); } private AmazonEC2 ec2Mock = mock(AmazonEC2.class); protected AmazonEC2 ec2Client() { return ec2Mock; } private AmazonAutoScalingClient asgMock = mock(AmazonAutoScalingClient.class); protected AmazonAutoScalingClient asgClient() { return asgMock; } protected AmazonEC2 superEc2Client() { return super.ec2Client(); } protected AmazonAutoScalingClient superAsgClient() { return super.asgClient(); } @Test public void testClients() { TestAWSClient client1 = new TestAWSClient(); Assert.assertNotNull(client1.superEc2Client(), "non null super ec2Client"); Assert.assertNotNull(client1.superAsgClient(), "non null super asgClient"); } @Test public void testTerminateInstance() { ArgumentCaptor arg = ArgumentCaptor.forClass(TerminateInstancesRequest.class); this.terminateInstance("fake:i-12345678901234567"); verify(ec2Mock).terminateInstances(arg.capture()); List instances = arg.getValue().getInstanceIds(); Assert.assertEquals(instances.size(), 1); Assert.assertEquals(instances.get(0), "fake:i-12345678901234567"); } private DescribeAutoScalingGroupsResult mkAsgResult(String asgName, String instanceId) { DescribeAutoScalingGroupsResult result = new DescribeAutoScalingGroupsResult(); AutoScalingGroup asg = new AutoScalingGroup(); asg.setAutoScalingGroupName(asgName); Instance inst = new Instance(); inst.setInstanceId(instanceId); asg.setInstances(Arrays.asList(inst)); result.setAutoScalingGroups(Arrays.asList(asg)); return result; } @Test public void testDescribeAutoScalingGroups() { DescribeAutoScalingGroupsResult result1 = mkAsgResult("asg1", "i-123456789012345670"); result1.setNextToken("nextToken"); DescribeAutoScalingGroupsResult result2 = mkAsgResult("asg2", "i-123456789012345671"); when(asgMock.describeAutoScalingGroups(any(DescribeAutoScalingGroupsRequest.class))).thenReturn(result1) .thenReturn(result2); List asgs = this.describeAutoScalingGroups(); verify(asgMock, times(2)).describeAutoScalingGroups(any(DescribeAutoScalingGroupsRequest.class)); Assert.assertEquals(asgs.size(), 2); // 2 asgs with 1 instance each Assert.assertEquals(asgs.get(0).getAutoScalingGroupName(), "asg1"); Assert.assertEquals(asgs.get(0).getInstances().size(), 1); Assert.assertEquals(asgs.get(0).getInstances().get(0).getInstanceId(), "i-123456789012345670"); Assert.assertEquals(asgs.get(1).getAutoScalingGroupName(), "asg2"); Assert.assertEquals(asgs.get(1).getInstances().size(), 1); Assert.assertEquals(asgs.get(1).getInstances().get(0).getInstanceId(), "i-123456789012345671"); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/client/aws/chaos/TestASGChaosCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.client.aws.chaos; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.testng.Assert; import org.testng.annotations.Test; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.Instance; import com.amazonaws.services.autoscaling.model.TagDescription; import com.netflix.simianarmy.basic.chaos.BasicInstanceGroup; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import com.netflix.simianarmy.client.aws.AWSClient; import com.netflix.simianarmy.tunable.TunableInstanceGroup; public class TestASGChaosCrawler { private final ASGChaosCrawler crawler; private AutoScalingGroup mkAsg(String asgName, String instanceId) { AutoScalingGroup asg = new AutoScalingGroup(); asg.setAutoScalingGroupName(asgName); Instance inst = new Instance(); inst.setInstanceId(instanceId); asg.setInstances(Arrays.asList(inst)); return asg; } private final AWSClient awsMock; public TestASGChaosCrawler() { awsMock = mock(AWSClient.class); crawler = new ASGChaosCrawler(awsMock); } @Test public void testGroupTypes() { EnumSet types = crawler.groupTypes(); Assert.assertEquals(types.size(), 1); Assert.assertEquals(types.iterator().next().name(), "ASG"); } @Test public void testGroups() { List asgList = new LinkedList(); asgList.add(mkAsg("asg1", "i-123456789012345670")); asgList.add(mkAsg("asg2", "i-123456789012345671")); when(awsMock.describeAutoScalingGroups((String[]) null)).thenReturn(asgList); List groups = crawler.groups(); verify(awsMock, times(1)).describeAutoScalingGroups((String[]) null); Assert.assertEquals(groups.size(), 2); Assert.assertEquals(groups.get(0).type(), ASGChaosCrawler.Types.ASG); Assert.assertEquals(groups.get(0).name(), "asg1"); Assert.assertEquals(groups.get(0).instances().size(), 1); Assert.assertEquals(groups.get(0).instances().get(0), "i-123456789012345670"); Assert.assertEquals(groups.get(1).type(), ASGChaosCrawler.Types.ASG); Assert.assertEquals(groups.get(1).name(), "asg2"); Assert.assertEquals(groups.get(1).instances().size(), 1); Assert.assertEquals(groups.get(1).instances().get(0), "i-123456789012345671"); } @Test public void testFindAggressionCoefficient() { AutoScalingGroup asg1 = mkAsg("asg1", "i-123456789012345670"); Set tagDescriptions = new HashSet<>(); tagDescriptions.add(makeTunableTag("1.0")); asg1.setTags(tagDescriptions); double aggression = crawler.findAggressionCoefficient(asg1); Assert.assertEquals(aggression, 1.0); } @Test public void testFindAggressionCoefficient_two() { AutoScalingGroup asg1 = mkAsg("asg1", "i-123456789012345670"); Set tagDescriptions = new HashSet<>(); tagDescriptions.add(makeTunableTag("2.0")); asg1.setTags(tagDescriptions); double aggression = crawler.findAggressionCoefficient(asg1); Assert.assertEquals(aggression, 2.0); } @Test public void testFindAggressionCoefficient_null() { AutoScalingGroup asg1 = mkAsg("asg1", "i-123456789012345670"); Set tagDescriptions = new HashSet<>(); tagDescriptions.add(makeTunableTag(null)); asg1.setTags(tagDescriptions); double aggression = crawler.findAggressionCoefficient(asg1); Assert.assertEquals(aggression, 1.0); } @Test public void testFindAggressionCoefficient_unparsable() { AutoScalingGroup asg1 = mkAsg("asg1", "i-123456789012345670"); Set tagDescriptions = new HashSet<>(); tagDescriptions.add(makeTunableTag("not a number")); asg1.setTags(tagDescriptions); double aggression = crawler.findAggressionCoefficient(asg1); Assert.assertEquals(aggression, 1.0); } private TagDescription makeTunableTag(String value) { TagDescription desc = new TagDescription(); desc.setKey("chaosMonkey.aggressionCoefficient"); desc.setValue(value); return desc; } @Test public void testGetInstanceGroup_basic() { AutoScalingGroup asg = mkAsg("asg1", "i-123456789012345670"); InstanceGroup group = crawler.getInstanceGroup(asg, 1.0); Assert.assertTrue( (group instanceof BasicInstanceGroup) ); Assert.assertFalse( (group instanceof TunableInstanceGroup) ); } @Test public void testGetInstanceGroup_tunable() { AutoScalingGroup asg = mkAsg("asg1", "i-123456789012345670"); InstanceGroup group = crawler.getInstanceGroup(asg, 2.0); Assert.assertTrue( (group instanceof TunableInstanceGroup) ); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/client/aws/chaos/TestFilterASGChaosCrawler.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.client.aws.chaos; import com.amazonaws.services.autoscaling.model.TagDescription; import com.netflix.simianarmy.GroupType; import com.netflix.simianarmy.basic.chaos.BasicInstanceGroup; import com.netflix.simianarmy.chaos.ChaosCrawler; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; import java.util.*; import static org.mockito.Mockito.*; import static org.testng.Assert.assertEquals; public class TestFilterASGChaosCrawler { private ChaosCrawler crawlerMock; private ChaosCrawler crawler; private String tagKey, tagValue; public enum Types implements GroupType { /** only crawls AutoScalingGroups. */ ASG; } @BeforeTest public void beforeTest() { crawlerMock = mock(ChaosCrawler.class); tagKey = "key-" + UUID.randomUUID().toString(); tagValue = "tagValue-" + UUID.randomUUID().toString(); crawler = new FilteringChaosCrawler(crawlerMock, new TagPredicate(tagKey, tagValue)); } @Test public void testFilterGroups() { List tagList = new ArrayList(); TagDescription td = new TagDescription(); td.setKey(tagKey); td.setValue(tagValue); tagList.add(td); List listGroup = new LinkedList(); listGroup.add(new BasicInstanceGroup("asg1", Types.ASG, "region1", tagList) ); listGroup.add(new BasicInstanceGroup("asg2", Types.ASG, "region2", Collections.emptyList()) ); listGroup.add(new BasicInstanceGroup("asg3", Types.ASG, "region3", tagList) ); listGroup.add(new BasicInstanceGroup("asg4", Types.ASG, "region4", Collections.emptyList()) ); when(crawlerMock.groups()).thenReturn(listGroup); List groups = crawlerMock.groups(); assertEquals(groups.size(), 4); groups = crawler.groups(); assertEquals(groups.size(), 2); assertEquals(groups.get(0).name(), "asg1"); assertEquals(groups.get(1).name(), "asg3"); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/client/vsphere/TestPropertyBasedTerminationStrategy.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ //CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.client.vsphere; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; import java.rmi.RemoteException; import org.testng.annotations.Test; import com.netflix.simianarmy.basic.BasicConfiguration; import com.vmware.vim25.mo.VirtualMachine; /** * @author ingmar.krusch@immobilienscout24.de */ public class TestPropertyBasedTerminationStrategy { private BasicConfiguration configMock = mock(BasicConfiguration.class); private VirtualMachine virtualMachineMock = mock(VirtualMachine.class); @Test public void shouldReturnConfiguredPropertyNameAndValueAfterConstructedFromConfig() { when(configMock.getStrOrElse("simianarmy.client.vsphere.terminationStrategy.property.name", "Force Boot")) .thenReturn("configured name"); when(configMock.getStrOrElse("simianarmy.client.vsphere.terminationStrategy.property.value", "server")) .thenReturn("configured value"); PropertyBasedTerminationStrategy strategy = new PropertyBasedTerminationStrategy(configMock); assertEquals(strategy.getPropertyName(), "configured name"); assertEquals(strategy.getPropertyValue(), "configured value"); } @Test public void shouldSetPropertyAndResetVirtualMachineAfterTermination() { when(configMock.getStrOrElse("simianarmy.client.vsphere.terminationStrategy.property.name", "Force Boot")) .thenReturn("configured name"); when(configMock.getStrOrElse("simianarmy.client.vsphere.terminationStrategy.property.value", "server")) .thenReturn("configured value"); PropertyBasedTerminationStrategy strategy = new PropertyBasedTerminationStrategy(configMock); try { strategy.terminate(virtualMachineMock); verify(virtualMachineMock, times(1)).setCustomValue("configured name", "configured value"); verify(virtualMachineMock, times(1)).resetVM_Task(); } catch (RemoteException e) { fail("termination should not fail", e); } } } ================================================ FILE: src/test/java/com/netflix/simianarmy/client/vsphere/TestVSpehereClient.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.client.vsphere; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertTrue; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import java.rmi.RemoteException; import java.util.List; import org.testng.annotations.Test; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.Instance; import com.vmware.vim25.mo.ManagedEntity; import com.vmware.vim25.mo.VirtualMachine; /** * @author ingmar.krusch@immobilienscout24.de */ public class TestVSpehereClient { @Test public void shouldTerminateCorrectly() throws RemoteException { VSphereServiceConnection connection = mock(VSphereServiceConnection.class); VirtualMachine vm1 = createVMMock("vm1"); when(connection.getVirtualMachineById("vm1")).thenReturn(vm1); TerminationStrategy strategy = mock(PropertyBasedTerminationStrategy.class); VSphereClient client = new VSphereClient(strategy, connection); client.terminateInstance("vm1"); verify(strategy, times(1)).terminate(vm1); } @Test public void shouldDescribeGroupsCorrectly() { VSphereServiceConnection connection = mock(VSphereServiceConnection.class); TerminationStrategy strategy = mock(PropertyBasedTerminationStrategy.class); VirtualMachine[] virtualMachines = {createVMMock("vm1"), createVMMock("vm2")}; when(connection.describeVirtualMachines()).thenReturn(virtualMachines); VSphereClient client = new VSphereClient(strategy, connection); List groups = client.describeAutoScalingGroups(); String str = flattenGroups(groups); assertTrue(groups.size() == 2, "did not desribes the 2 vm's that were given"); assertTrue(str.indexOf("group:vm1.parent.name:id:vm1.name:") >= 0, "did not describe vm1 correctly"); assertTrue(str.indexOf("group:vm2.parent.name:id:vm2.name:") >= 0, "did not describe vm2 correctly"); } private String flattenGroups(List groups) { StringBuilder buf = new StringBuilder(); for (AutoScalingGroup asg : groups) { List instances = asg.getInstances(); buf.append("group:").append(asg.getAutoScalingGroupName()).append(":"); for (Instance instance : instances) { buf.append("id:").append(instance.getInstanceId()).append(":"); } } return buf.toString(); } private VirtualMachine createVMMock(String id) { VirtualMachine vm1 = mock(VirtualMachine.class); ManagedEntity me1 = mock(ManagedEntity.class); when(vm1.getName()).thenReturn(id + ".name"); when(vm1.getParent()).thenReturn(me1); when(me1.getName()).thenReturn(id + ".parent.name"); return vm1; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/client/vsphere/TestVSphereContext.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.client.vsphere; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import org.testng.annotations.Test; import com.netflix.simianarmy.client.aws.AWSClient; /** * @author ingmar.krusch@immobilienscout24.de */ public class TestVSphereContext { @Test public void shouldSetClientOfCorrectType() { VSphereContext context = new VSphereContext(); AWSClient awsClient = context.awsClient(); assertNotNull(awsClient); assertTrue(awsClient instanceof VSphereClient); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/client/vsphere/TestVSphereGroups.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.client.vsphere; import static org.testng.Assert.assertEquals; import java.util.List; import org.testng.annotations.Test; import com.amazonaws.services.autoscaling.model.AutoScalingGroup; import com.amazonaws.services.autoscaling.model.Instance; /** * @author ingmar.krusch@immobilienscout24.de */ public class TestVSphereGroups { @Test public void shouldReturnListContainigSingleASGWhenAddInstanceIsCalledOnce() { VSphereGroups groups = new VSphereGroups(); groups.addInstance("anyInstanceId", "anyGroupName"); List list = groups.asList(); assertEquals(1, list.size()); AutoScalingGroup firstItem = list.get(0); assertEquals("anyGroupName", firstItem.getAutoScalingGroupName()); List instances = firstItem.getInstances(); assertEquals(1, instances.size()); assertEquals("anyInstanceId", instances.get(0).getInstanceId()); } @Test public void shouldReturnListContainingSingleASGWithTwoInstancesWhenAddInstanceIsCaledTwiceForSameGroup() { VSphereGroups groups = new VSphereGroups(); groups.addInstance("anyInstanceId", "anyGroupName"); groups.addInstance("anyOtherInstanceId", "anyGroupName"); List list = groups.asList(); assertEquals(1, list.size()); List instances = list.get(0).getInstances(); assertEquals(2, instances.size()); assertEquals("anyInstanceId", instances.get(0).getInstanceId()); assertEquals("anyOtherInstanceId", instances.get(1).getInstanceId()); } @Test public void shouldReturnListContainigTwoASGWhenAddInstanceIsCalledTwice() { VSphereGroups groups = new VSphereGroups(); groups.addInstance("anyInstanceId", "anyGroupName"); groups.addInstance("anyOtherInstanceId", "anyOtherGroupName"); List list = groups.asList(); assertEquals(2, list.size()); AutoScalingGroup firstGroup = list.get(0); assertEquals("anyGroupName", firstGroup.getAutoScalingGroupName()); List firstGroupInstances = firstGroup.getInstances(); assertEquals(1, firstGroupInstances.size()); assertEquals("anyInstanceId", firstGroupInstances.get(0).getInstanceId()); AutoScalingGroup secondGroup = list.get(1); assertEquals("anyOtherGroupName", secondGroup.getAutoScalingGroupName()); List secondGroupInstances = secondGroup.getInstances(); assertEquals(1, secondGroupInstances.size()); assertEquals("anyOtherInstanceId", secondGroupInstances.get(0).getInstanceId()); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/client/vsphere/TestVSphereServiceConnection.java ================================================ /* * Copyright 2012 Immobilien Scout GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.client.vsphere; import static com.netflix.simianarmy.client.vsphere.VSphereServiceConnection.VIRTUAL_MACHINE_TYPE_NAME; import static junit.framework.Assert.assertSame; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import java.rmi.RemoteException; import org.testng.Assert; import org.testng.annotations.Test; import com.amazonaws.AmazonServiceException; import com.netflix.simianarmy.basic.BasicConfiguration; import com.vmware.vim25.InvalidProperty; import com.vmware.vim25.RuntimeFault; import com.vmware.vim25.mo.InventoryNavigator; import com.vmware.vim25.mo.ManagedEntity; import com.vmware.vim25.mo.VirtualMachine; /** * @author ingmar.krusch@immobilienscout24.de */ public class TestVSphereServiceConnection { // private ServiceInstance serviceMock = mock(ServiceInstance.class); private BasicConfiguration configMock = mock(BasicConfiguration.class); @Test public void shouldReturnConfiguredPropertiesAfterConstructedFromConfig() { when(configMock.getStr("simianarmy.client.vsphere.username")).thenReturn("configured username"); when(configMock.getStr("simianarmy.client.vsphere.password")).thenReturn("configured password"); when(configMock.getStr("simianarmy.client.vsphere.url")).thenReturn("configured url"); VSphereServiceConnection service = new VSphereServiceConnection(configMock); assertEquals(service.getUsername(), "configured username"); assertEquals(service.getPassword(), "configured password"); assertEquals(service.getUrl(), "configured url"); } @Test public void shouldCallSearchManagedEntityAndReturnVMForDoItGetVirtualMachineById() throws RemoteException { VSphereServiceConnectionWithMockedInventoryNavigator service = new VSphereServiceConnectionWithMockedInventoryNavigator(); InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock(); VirtualMachine vmMock = mock(VirtualMachine.class); when(inventoryNavigatorMock.searchManagedEntity(VIRTUAL_MACHINE_TYPE_NAME, "instanceId")).thenReturn(vmMock); VirtualMachine actualVM = service.getVirtualMachineById("instanceId"); verify(inventoryNavigatorMock).searchManagedEntity(VIRTUAL_MACHINE_TYPE_NAME, "instanceId"); assertSame(vmMock, actualVM); } @Test //(expectedExceptions = AmazonServiceException.class) public void shouldThrowExceptionWhenCallingSearchManagedEntitiesOnDescribeWhenNoVMsAreReturned() throws RemoteException { VSphereServiceConnectionWithMockedInventoryNavigator service = new VSphereServiceConnectionWithMockedInventoryNavigator(); try { service.describeVirtualMachines(); } catch (AmazonServiceException e) { Assert.assertTrue(e != null); } } @Test public void shouldCallSearchManagedEntitiesOnDescribeWhenAtLeastOneVMIsReturned() throws RemoteException { VSphereServiceConnectionWithMockedInventoryNavigator service = new VSphereServiceConnectionWithMockedInventoryNavigator(); InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock(); ManagedEntity[] meMocks = new ManagedEntity[] {mock(VirtualMachine.class)}; when(inventoryNavigatorMock.searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME)).thenReturn(meMocks); VirtualMachine[] actualVMs = service.describeVirtualMachines(); verify(inventoryNavigatorMock).searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME); assertSame(meMocks[0], actualVMs[0]); } @Test(expectedExceptions = AmazonServiceException.class) public void shouldEncapsulateInvalidPropertyException() throws RemoteException { VSphereServiceConnectionWithMockedInventoryNavigator service = new VSphereServiceConnectionWithMockedInventoryNavigator(); InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock(); when(inventoryNavigatorMock.searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME)).thenThrow(new InvalidProperty()); service.describeVirtualMachines(); } @Test(expectedExceptions = AmazonServiceException.class) public void shouldEncapsulateRuntimeFaultException() throws RemoteException { VSphereServiceConnectionWithMockedInventoryNavigator service = new VSphereServiceConnectionWithMockedInventoryNavigator(); InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock(); when(inventoryNavigatorMock.searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME)).thenThrow(new RuntimeFault()); service.describeVirtualMachines(); } @Test(expectedExceptions = AmazonServiceException.class) public void shouldEncapsulateRemoteExceptionException() throws RemoteException { VSphereServiceConnectionWithMockedInventoryNavigator service = new VSphereServiceConnectionWithMockedInventoryNavigator(); InventoryNavigator inventoryNavigatorMock = service.getInventoryNavigatorMock(); when(inventoryNavigatorMock.searchManagedEntities(VIRTUAL_MACHINE_TYPE_NAME)).thenThrow(new RemoteException()); service.describeVirtualMachines(); } // The API class ServerConnection is final and can therefore not be mocked. // It's possible to work around this using a wrapper, but this is a lot of // fake code that needs to be written and tested again just to test that // this code really calls the interface method. This is something that rather // should be tested in a system test. //@Test // public void shouldDisconnectSeviceByLogoutOverConnection() { // VSphereServiceConnectionWithMockedConnection connection = // new VSphereServiceConnectionWithMockedConnection(); // // ServiceInstance serviceMock = connection.getService(); // ServerConnection serverConnectionMock = mock(ServerConnection.class); // when(serviceMock.getServerConnection()).thenReturn(serverConnectionMock); // // connection.disconnect(); // // verify(serviceMock).getServerConnection(); // verify(serverConnectionMock).logout(); // assertNull(connection.getService()); // } } //class VSphereServiceConnectionWithMockedConnection extends VSphereServiceConnection { // public VSphereServiceConnectionWithMockedConnection() { // super(mock(BasicConfiguration.class)); // this.setService(mock(ServiceInstance.class)); // } //} class VSphereServiceConnectionWithMockedInventoryNavigator extends VSphereServiceConnection { private InventoryNavigator inventoryNavigatorMock = mock(InventoryNavigator.class); public VSphereServiceConnectionWithMockedInventoryNavigator() { super(mock(BasicConfiguration.class)); } @Override protected InventoryNavigator getInventoryNavigator() { return inventoryNavigatorMock; } public InventoryNavigator getInventoryNavigatorMock() { return inventoryNavigatorMock; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/conformity/TestCrossZoneLoadBalancing.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.conformity; import java.util.Arrays; import java.util.List; import java.util.Map; import junit.framework.Assert; import org.apache.commons.lang.StringUtils; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import com.google.common.collect.Maps; import com.netflix.simianarmy.aws.conformity.rule.CrossZoneLoadBalancing; public class TestCrossZoneLoadBalancing extends CrossZoneLoadBalancing { private final Map asgToElbs = Maps.newHashMap(); private final Map elbsToCZLB = Maps.newHashMap(); @BeforeClass private void init() { asgToElbs.put("asg1", "elb1,elb2"); asgToElbs.put("asg2", "elb1"); asgToElbs.put("asg3", ""); elbsToCZLB.put("elb1", true); } @Test public void testDisabledCrossZoneLoadBalancing() { Cluster cluster = new Cluster("cluster1", "us-east-1", new AutoScalingGroup("asg1")); Conformity result = check(cluster); Assert.assertEquals(result.getRuleId(), getName()); Assert.assertEquals(result.getFailedComponents().size(), 1); Assert.assertEquals(result.getFailedComponents().iterator().next(), "elb2"); } @Test public void testEnabledCrossZoneLoadBalancing() { Cluster cluster = new Cluster("cluster1", "us-east-1", new AutoScalingGroup("asg2")); Conformity result = check(cluster); Assert.assertEquals(result.getRuleId(), getName()); Assert.assertEquals(result.getFailedComponents().size(), 0); } @Test public void testAsgWithoutElb() { Cluster cluster = new Cluster("cluster3", "us-east-1", new AutoScalingGroup("asg3")); Conformity result = check(cluster); Assert.assertEquals(result.getRuleId(), getName()); Assert.assertEquals(result.getFailedComponents().size(), 0); } @Override protected List getLoadBalancerNamesForAsg(String region, String asgName) { return Arrays.asList(StringUtils.split(asgToElbs.get(asgName), ",")); } @Override protected boolean isCrossZoneLoadBalancingEnabled(String region, String lbName) { Boolean enabled = elbsToCZLB.get(lbName); return (enabled == null) ? false : enabled; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/conformity/TestSameZonesInElbAndAsg.java ================================================ /* * * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc package com.netflix.simianarmy.conformity; import com.google.common.collect.Maps; import com.netflix.simianarmy.aws.conformity.rule.SameZonesInElbAndAsg; import junit.framework.Assert; import org.apache.commons.lang.StringUtils; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import java.util.Arrays; import java.util.List; import java.util.Map; public class TestSameZonesInElbAndAsg extends SameZonesInElbAndAsg { private final Map asgToElbs = Maps.newHashMap(); private final Map asgToZones = Maps.newHashMap(); private final Map elbToZones = Maps.newHashMap(); @BeforeClass private void init() { asgToElbs.put("asg1", "elb1,elb2"); asgToElbs.put("asg2", "elb2"); asgToElbs.put("asg3", ""); asgToZones.put("asg1", "us-east-1a,us-east-1b"); asgToZones.put("asg2", "us-east-1a"); asgToZones.put("asg3", "us-east-1b"); elbToZones.put("elb1", "us-east-1a,us-east-1b"); elbToZones.put("elb2", "us-east-1a"); } @Test public void testZoneMismatch() { Cluster cluster = new Cluster("cluster1", "us-east-1", new AutoScalingGroup("asg1")); Conformity result = check(cluster); Assert.assertEquals(result.getRuleId(), getName()); Assert.assertEquals(result.getFailedComponents().size(), 1); Assert.assertEquals(result.getFailedComponents().iterator().next(), "elb2"); } @Test public void testZoneMatch() { Cluster cluster = new Cluster("cluster2", "us-east-1", new AutoScalingGroup("asg2")); Conformity result = check(cluster); Assert.assertEquals(result.getRuleId(), getName()); Assert.assertEquals(result.getFailedComponents().size(), 0); } @Test public void testAsgWithoutElb() { Cluster cluster = new Cluster("cluster3", "us-east-1", new AutoScalingGroup("asg3")); Conformity result = check(cluster); Assert.assertEquals(result.getRuleId(), getName()); Assert.assertEquals(result.getFailedComponents().size(), 0); } @Override protected List getLoadBalancerNamesForAsg(String region, String asgName) { return Arrays.asList(StringUtils.split(asgToElbs.get(asgName), ",")); } @Override protected List getAvailabilityZonesForAsg(String region, String asgName) { return Arrays.asList(StringUtils.split(asgToZones.get(asgName), ",")); } @Override protected List getAvailabilityZonesForLoadBalancer(String region, String lbName) { return Arrays.asList(StringUtils.split(elbToZones.get(lbName), ",")); } } ================================================ FILE: src/test/java/com/netflix/simianarmy/janitor/TestAbstractJanitor.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc // CHECKSTYLE IGNORE MagicNumberCheck package com.netflix.simianarmy.janitor; import com.netflix.simianarmy.*; import com.netflix.simianarmy.Resource.CleanupState; import com.netflix.simianarmy.aws.AWSResource; import com.netflix.simianarmy.aws.janitor.rule.TestMonkeyCalendar; import com.netflix.simianarmy.basic.BasicConfiguration; import com.netflix.simianarmy.basic.janitor.BasicJanitorRuleEngine; import org.joda.time.DateTime; import org.testng.Assert; import org.testng.annotations.Test; import java.util.*; public class TestAbstractJanitor extends AbstractJanitor { private static final String TEST_REGION = "test-region"; public TestAbstractJanitor(AbstractJanitor.Context ctx, ResourceType resourceType) { super(ctx, resourceType); this.idToResource = new HashMap<>(); for (Resource r : ((TestJanitorCrawler) (ctx.janitorCrawler())).getCrawledResources()) { this.idToResource.put(r.getId(), r); } } // The collection of all resources for testing. private final Map idToResource; private final HashSet markedResourceIds = new HashSet<>(); private final HashSet cleanedResourceIds = new HashSet<>(); @Override protected void postMark(Resource resource) { markedResourceIds.add(resource.getId()); } @Override protected void cleanup(Resource resource) { if (!idToResource.containsKey(resource.getId())) { throw new RuntimeException(); } // add a special case to throw exception if (resource.getId().equals("11")) { throw new RuntimeException("Magic number of id."); } idToResource.remove(resource.getId()); } @Override public void cleanupDryRun(Resource resource) throws DryRunnableJanitorException { // simulates a dryRun try { if (!idToResource.containsKey(resource.getId())) { throw new RuntimeException(); } if (resource.getId().equals("11")) { throw new RuntimeException("Magic number of id."); } } catch (Exception e) { throw new DryRunnableJanitorException("Exception during dry run", e); } } @Override protected void postCleanup(Resource resource) { cleanedResourceIds.add(resource.getId()); } private static List generateTestingResources(int n) { List resources = new ArrayList(n); for (int i = 1; i <= n; i++) { resources.add(new AWSResource().withId(String.valueOf(i)) .withRegion(TEST_REGION) .withResourceType(TestResourceType.TEST_RESOURCE_TYPE) .withOptOutOfJanitor(false)); } return resources; } @Test public static void testJanitor() { int n = 10; Collection crawledResources = new ArrayList<>(generateTestingResources(n)); TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(new HashMap<>()); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new IsEvenRule()), crawler, resourceTracker, new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(false); Assert.assertEquals(crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(), n); Assert.assertEquals(janitor.markedResourceIds.size(), 0); janitor.markResources(); Assert.assertEquals(janitor.getMarkedResources().size(), n / 2); Assert.assertEquals(janitor.markedResourceIds.size(), n / 2); for (int i = 1; i <= n; i += 2) { Assert.assertTrue(janitor.markedResourceIds.contains(String.valueOf(i))); } Assert.assertEquals(janitor.cleanedResourceIds.size(), 0); janitor.cleanupResources(); Assert.assertEquals(janitor.getCleanedResources().size(), n / 2); Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.JANITOR_TERMINATED, TEST_REGION).size(), n / 2); Assert.assertEquals(janitor.cleanedResourceIds.size(), n / 2); for (int i = 1; i <= n; i += 2) { Assert.assertTrue(janitor.cleanedResourceIds.contains(String.valueOf(i))); } Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size()); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0); } @Test public static void testJanitorWithOptedOutResources() { Collection crawledResources = new ArrayList(); int n = 10; for (Resource r : generateTestingResources(n)) { crawledResources.add(r); } TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); // set some resources in the tracker as opted out Date now = new Date(DateTime.now().minusDays(1).getMillis()); Map trackedResources = new HashMap(); for (Resource r : generateTestingResources(n)) { int id = Integer.parseInt(r.getId()); if (id % 4 == 1 || id % 4 == 2) { r.setOptOutOfJanitor(true); r.setState(CleanupState.MARKED); r.setExpectedTerminationTime(now); r.setMarkTime(now); } trackedResources.put(r.getId(), r); } TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker( trackedResources); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new IsEvenRule()), crawler, resourceTracker, new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(false); Assert.assertEquals( crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(), 10); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(), 6); // 1, 2, 5, 6, 9, 10 are marked Assert.assertEquals(janitor.markedResourceIds.size(), 0); janitor.markResources(); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(), 5); // 1, 3, 5, 7, 9 are marked Assert.assertEquals(janitor.getMarkedResources().size(), 2); // 3, 7 are newly marked. Assert.assertEquals(janitor.markedResourceIds.size(), 2); Assert.assertEquals(janitor.cleanedResourceIds.size(), 0); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(), 5); // 1, 3, 5, 7, 9 are marked Assert.assertEquals(janitor.getUnmarkedResources().size(), 3); // 2, 6, 10 got unmarked Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.UNMARKED, TEST_REGION).size(), 3); janitor.cleanupResources(); Assert.assertEquals(janitor.getCleanedResources().size(), 2); // 3, 7 are cleaned Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.JANITOR_TERMINATED, TEST_REGION).size(), 2); Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size()); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0); Assert.assertEquals(janitor.getUnmarkedResourcesCount(), 3); } @Test public static void testJanitorWithCleanupFailure() { Collection crawledResources = new ArrayList(); int n = 20; for (Resource r : generateTestingResources(n)) { crawledResources.add(r); } TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new IsEvenRule()), crawler, new TestJanitorResourceTracker(new HashMap()), new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(false); Assert.assertEquals( crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(), n); janitor.markResources(); Assert.assertEquals(janitor.getMarkedResources().size(), n / 2); janitor.cleanupResources(); Assert.assertEquals(janitor.getCleanedResources().size(), n / 2 - 1); Assert.assertEquals(janitor.getFailedToCleanResources().size(), 1); Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size()); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 1); } private static TestAbstractJanitor getJanitor(int numberOfCrawledResources, boolean leashed) { TestJanitorCrawler crawler = new TestJanitorCrawler(generateTestingResources(numberOfCrawledResources)); JanitorRuleEngine rulesEngine = new BasicJanitorRuleEngine().addRule(new IsEvenRule()); JanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(new HashMap<>()); TestJanitorContext janitorContext = new TestJanitorContext(TEST_REGION, rulesEngine, crawler, resourceTracker, new TestMonkeyCalendar()); TestAbstractJanitor janitor = new TestAbstractJanitor(janitorContext, TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(leashed); return janitor; } @Test public static void testCleanupDryRunOnWithJanitorOnLeashWithAFailure() { int n = 20; TestAbstractJanitor janitor = getJanitor(n, true); janitor.markResources(); Assert.assertEquals(janitor.getMarkedResources().size(), n / 2); janitor.cleanupResources(); Assert.assertEquals(janitor.getCleanedResources().size(), 0); Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0); Assert.assertEquals(janitor.getResourcesCleanedCount(), 0); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0); Assert.assertEquals(janitor.getCleanupDryRunFailureCount().getValue().intValue(), 1); } @Test public static void testJanitorWithUnmarking() { Collection crawledResources = new ArrayList(); Map trackedResources = new HashMap(); int n = 10; DateTime now = DateTime.now(); Date markTime = new Date(now.minusDays(5).getMillis()); Date notifyTime = new Date(now.minusDays(4).getMillis()); Date terminationTime = new Date(now.minusDays(1).getMillis()); for (Resource r : generateTestingResources(n)) { if (Integer.parseInt(r.getId()) % 3 == 0) { trackedResources.put(r.getId(), r); r.setState(CleanupState.MARKED); r.setMarkTime(markTime); r.setExpectedTerminationTime(terminationTime); r.setNotificationTime(notifyTime); } } for (Resource r : generateTestingResources(n)) { crawledResources.add(r); } TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(trackedResources); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new IsEvenRule()), crawler, resourceTracker, new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(false); Assert.assertEquals( crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(), n); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(), n / 3); janitor.markResources(); // (n/3-n/6) resources were already marked, so in the last run the marked resources // should be n/2 - n/3 + n/6. Assert.assertEquals(janitor.getMarkedResources().size(), n / 2 - n / 3 + n / 6); Assert.assertEquals(janitor.getUnmarkedResources().size(), n / 6); janitor.cleanupResources(); Assert.assertEquals(janitor.getCleanedResources().size(), n / 2); Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0); Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size()); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0); Assert.assertEquals(janitor.getUnmarkedResourcesCount(), n/6); } @Test public static void testJanitorWithFutureTerminationTime() { Collection crawledResources = new ArrayList(); Map trackedResources = new HashMap(); int n = 10; DateTime now = DateTime.now(); Date markTime = new Date(now.minusDays(5).getMillis()); Date notifyTime = new Date(now.minusDays(4).getMillis()); Date terminationTime = new Date(now.plusDays(10).getMillis()); for (Resource r : generateTestingResources(n)) { trackedResources.put(r.getId(), r); r.setState(CleanupState.MARKED); r.setNotificationTime(notifyTime); r.setMarkTime(markTime); r.setExpectedTerminationTime(terminationTime); } for (Resource r : generateTestingResources(n)) { crawledResources.add(r); } TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(trackedResources); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new IsEvenRule()), crawler, resourceTracker, new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(false); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(), n); janitor.cleanupResources(); Assert.assertEquals(janitor.getCleanedResources().size(), 0); Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0); Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size()); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0); } @Test public static void testJanitorWithoutNotification() { Collection crawledResources = new ArrayList(); Map trackedResources = new HashMap(); int n = 10; for (Resource r : generateTestingResources(n)) { trackedResources.put(r.getId(), r); r.setState(CleanupState.MARKED); // The marking/cleanup is not notified so we the Janitor won't clean it up. // r.setNotificationTime(new Date()); r.setMarkTime(new Date()); r.setExpectedTerminationTime(new Date(DateTime.now().plusDays(10).getMillis())); } for (Resource r : generateTestingResources(n)) { crawledResources.add(r); } TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(trackedResources); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new IsEvenRule()), crawler, resourceTracker, new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(false); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(), n); janitor.cleanupResources(); Assert.assertEquals(janitor.getCleanedResources().size(), 0); Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0); Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size()); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0); } @Test public static void testLeashedJanitorForMarking() { Collection crawledResources = new ArrayList(); int n = 10; for (Resource r : generateTestingResources(n)) { crawledResources.add(r); } TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker( new HashMap()); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new IsEvenRule()), crawler, resourceTracker, new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(true); Assert.assertEquals( crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(), n); janitor.markResources(); Assert.assertEquals(janitor.getMarkedResources().size(), n / 2); Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), n / 2); } @Test public static void testJanitorWithoutHoldingOffCleanup() { Collection crawledResources = new ArrayList(); int n = 10; for (Resource r : generateTestingResources(n)) { crawledResources.add(r); } TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(new HashMap()); DateTime now = DateTime.now(); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new ImmediateCleanupRule(now)), crawler, resourceTracker, new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(false); Assert.assertEquals( crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(), n); Assert.assertEquals(janitor.markedResourceIds.size(), 0); janitor.markResources(); Assert.assertEquals(janitor.getMarkedResources().size(), n); Assert.assertEquals(janitor.markedResourceIds.size(), n); for (int i = 1; i <= n; i++) { Assert.assertTrue(janitor.markedResourceIds.contains(String.valueOf(i))); } Assert.assertEquals(janitor.cleanedResourceIds.size(), 0); janitor.cleanupResources(); // No resource is cleaned since the notification is later than expected termination time. Assert.assertEquals(janitor.getCleanedResources().size(), n); Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.JANITOR_TERMINATED, TEST_REGION).size(), n); Assert.assertEquals(janitor.cleanedResourceIds.size(), n); Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size()); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0); } // @Test TODO: disable while debugging issues with this functionality public static void testJanitorWithUnmarkingUserTerminated() { Collection crawledResources = new ArrayList(); Map trackedResources = new HashMap(); int n = 10; DateTime now = DateTime.now(); Date markTime = new Date(now.minusDays(5).getMillis()); Date notifyTime = new Date(now.minusDays(4).getMillis()); Date terminationTime = new Date(now.minusDays(1).getMillis()); for (Resource r : generateTestingResources(n)) { if (Integer.parseInt(r.getId()) % 3 != 0) { crawledResources.add(r); } else { trackedResources.put(r.getId(), r); r.setState(CleanupState.MARKED); r.setMarkTime(markTime); r.setNotificationTime(notifyTime); r.setExpectedTerminationTime(terminationTime); } } TestJanitorCrawler crawler = new TestJanitorCrawler(crawledResources); TestJanitorResourceTracker resourceTracker = new TestJanitorResourceTracker(trackedResources); TestAbstractJanitor janitor = new TestAbstractJanitor( new TestJanitorContext(TEST_REGION, new BasicJanitorRuleEngine().addRule(new IsEvenRule()), crawler, resourceTracker, new TestMonkeyCalendar()), TestResourceType.TEST_RESOURCE_TYPE); janitor.setLeashed(false); Assert.assertEquals( crawler.resources(TestResourceType.TEST_RESOURCE_TYPE).size(), n - n / 3); Assert.assertEquals(resourceTracker.getResources( TestResourceType.TEST_RESOURCE_TYPE, CleanupState.MARKED, TEST_REGION).size(), n / 3); janitor.markResources(); // n/3 resources should be considered user terminated Assert.assertEquals(janitor.getMarkedResources().size(), n / 2 - n / 3 + n / 6); Assert.assertEquals(janitor.getUnmarkedResources().size(), n / 3); janitor.cleanupResources(); Assert.assertEquals(janitor.getCleanedResources().size(), n / 2 - n / 3 + n / 6); Assert.assertEquals(janitor.getFailedToCleanResources().size(), 0); Assert.assertEquals(janitor.getResourcesCleanedCount(), janitor.cleanedResourceIds.size()); Assert.assertEquals(janitor.getMarkedResourcesCount(), janitor.markedResourceIds.size()); Assert.assertEquals(janitor.getFailedToCleanResourcesCount(), 0); Assert.assertEquals(janitor.getUnmarkedResourcesCount(), n / 3); } } class TestJanitorCrawler implements JanitorCrawler { private final Collection crawledResources; public Collection getCrawledResources() { return crawledResources; } public TestJanitorCrawler(Collection crawledResources) { this.crawledResources = crawledResources; } @Override public EnumSet resourceTypes() { return EnumSet.of(TestResourceType.TEST_RESOURCE_TYPE); } @Override public List resources(ResourceType resourceType) { return new ArrayList(crawledResources); } @Override public List resources(String... resourceIds) { List result = new ArrayList(resourceIds.length); Set idSet = new HashSet(Arrays.asList(resourceIds)); for (Resource r : crawledResources) { if (idSet.contains(r.getId())) { result.add(r); } } return result; } @Override public String getOwnerEmailForResource(Resource resource) { return null; } } enum TestResourceType implements ResourceType { TEST_RESOURCE_TYPE } class TestJanitorResourceTracker implements JanitorResourceTracker { private final Map resources; public TestJanitorResourceTracker(Map trackedResources) { this.resources = trackedResources; } @Override public void addOrUpdate(Resource resource) { resources.put(resource.getId(), resource); } @Override public List getResources(ResourceType resourceType, CleanupState state, String region) { List result = new ArrayList(); for (Resource r : resources.values()) { if (r.getResourceType().equals(resourceType) && (r.getState() != null && r.getState().equals(state)) && r.getRegion().equals(region)) { result.add(r.cloneResource()); } } return result; } @Override public Resource getResource(String resourceId) { return resources.get(resourceId); } @Override public Resource getResource(String resourceId, String region) { return resources.get(resourceId); } } /** * The rule considers all resources with an odd number as the id as cleanup candidate. */ class IsEvenRule implements Rule { @Override public boolean isValid(Resource resource) { // returns true if the resource's id is an even integer int id; try { id = Integer.parseInt(resource.getId()); } catch (Exception e) { return true; } DateTime now = DateTime.now(); resource.setExpectedTerminationTime(new Date(now.minusDays(1).getMillis())); // Set the resource as notified so it can be cleaned // set the notification time at more than 1 day before the termination time resource.setNotificationTime(new Date(now.minusDays(4).getMillis())); return id % 2 == 0; } } /** * The rule considers all resources as cleanup candidate and sets notification time * after the termination time. */ class ImmediateCleanupRule implements Rule { private final DateTime now; public ImmediateCleanupRule(DateTime now) { this.now = now; } @Override public boolean isValid(Resource resource) { resource.setExpectedTerminationTime(new Date(now.minusMinutes(10).getMillis())); resource.setNotificationTime(new Date(now.getMillis()-5000)); return false; } } class TestJanitorContext implements AbstractJanitor.Context { private final String region; private final JanitorRuleEngine ruleEngine; private final JanitorCrawler crawler; private final JanitorResourceTracker resourceTracker; private final MonkeyCalendar calendar; public TestJanitorContext(String region, JanitorRuleEngine ruleEngine, JanitorCrawler crawler, JanitorResourceTracker resourceTracker, MonkeyCalendar calendar) { this.region = region; this.resourceTracker = resourceTracker; this.ruleEngine = ruleEngine; this.crawler = crawler; this.calendar = calendar; } @Override public String region() { return region; } @Override public MonkeyCalendar calendar() { return calendar; } @Override public JanitorRuleEngine janitorRuleEngine() { return ruleEngine; } @Override public JanitorCrawler janitorCrawler() { return crawler; } @Override public JanitorResourceTracker janitorResourceTracker() { return resourceTracker; } @Override public MonkeyConfiguration configuration() { return new BasicConfiguration(new Properties()); } @Override public MonkeyRecorder recorder() { // No events to be recorded return null; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/janitor/TestBasicJanitorMonkeyContext.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.janitor; import com.netflix.simianarmy.aws.janitor.rule.generic.UntaggedRule; import com.netflix.simianarmy.basic.TestBasicCalendar; import com.netflix.simianarmy.basic.janitor.BasicJanitorRuleEngine; import org.apache.commons.lang.StringUtils; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** * The basic implementation of the context class for Janitor monkey. */ public class TestBasicJanitorMonkeyContext { private static final int SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOWNER = 3; private static final int SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOUTOWNER = 8; private static final Boolean SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_ENABLED = true; private static final Set SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_REQUIREDTAGS = new HashSet(Arrays.asList("owner", "costcenter")); private static final String SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RESOURCES = "Instance"; private String monkeyRegion; private TestBasicCalendar monkeyCalendar; public TestBasicJanitorMonkeyContext() { super(); } @BeforeMethod public void before() { monkeyRegion = "us-east-1"; monkeyCalendar = new TestBasicCalendar(); } @Test public void testAddRuleWithUntaggedRuleResource() { JanitorRuleEngine ruleEngine = new BasicJanitorRuleEngine(); Boolean untaggedRuleEnabled = new Boolean(true); Rule rule = new UntaggedRule(monkeyCalendar, SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_REQUIREDTAGS, SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOWNER, SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOUTOWNER); if (untaggedRuleEnabled && getUntaggedRuleResourceSet().contains("INSTANCE")) { ruleEngine.addRule(rule); } Assert.assertTrue(ruleEngine.getRules().contains(rule)); } @Test public void testAddRuleWithoutUntaggedRuleResource() { JanitorRuleEngine ruleEngine = new BasicJanitorRuleEngine(); Boolean untaggedRuleEnabled = new Boolean(true); Rule rule = new UntaggedRule(monkeyCalendar, SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_REQUIREDTAGS, SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOWNER, SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RETENTIONDAYSWITHOUTOWNER); if (untaggedRuleEnabled && getUntaggedRuleResourceSet().contains("ASG")) { ruleEngine.addRule(rule); } Assert.assertFalse(ruleEngine.getRules().contains(rule)); } private Set getUntaggedRuleResourceSet() { Set untaggedRuleResourceSet = new HashSet(); if (SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_ENABLED) { String untaggedRuleResources = SIMIANARMY_JANITOR_RULE_UNTAGGEDRULE_RESOURCES; if (StringUtils.isNotBlank(untaggedRuleResources)) { for (String resourceType : untaggedRuleResources.split(",")) { untaggedRuleResourceSet.add(resourceType.trim().toUpperCase()); } } } return untaggedRuleResourceSet; } } ================================================ FILE: src/test/java/com/netflix/simianarmy/resources/chaos/TestChaosMonkeyResource.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // CHECKSTYLE IGNORE Javadoc //CHECKSTYLE IGNORE MagicNumber package com.netflix.simianarmy.resources.chaos; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMap; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Arrays; import java.util.Date; import java.util.Map; import java.util.Scanner; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; import com.netflix.simianarmy.EventType; import com.netflix.simianarmy.MonkeyRecorder; import com.netflix.simianarmy.MonkeyRunner; import com.netflix.simianarmy.MonkeyType; import com.netflix.simianarmy.basic.BasicRecorderEvent; import com.netflix.simianarmy.basic.chaos.BasicChaosMonkey; import com.netflix.simianarmy.chaos.ChaosMonkey; import com.netflix.simianarmy.chaos.TestChaosMonkeyContext; import com.sun.jersey.core.util.MultivaluedMapImpl; public class TestChaosMonkeyResource { private static final Logger LOGGER = LoggerFactory.getLogger(TestChaosMonkeyResource.class); @Captor private ArgumentCaptor monkeyTypeArg; @Captor private ArgumentCaptor eventTypeArg; @Captor private ArgumentCaptor> queryArg; @Captor private ArgumentCaptor dateArg; @Mock private UriInfo mockUriInfo; @Mock private static MonkeyRecorder mockRecorder; @BeforeTest public void init() { MockitoAnnotations.initMocks(this); } @Test void testTerminateNow() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("ondemandTermination.properties"); String input = "{\"eventType\":\"CHAOS_TERMINATION\",\"groupType\":\"TYPE_C\",\"groupName\":\"name4\"}"; Assert.assertEquals(ctx.selectedOn().size(), 0); Assert.assertEquals(ctx.terminated().size(), 0); ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.OK); Assert.assertEquals(ctx.selectedOn().size(), 1); Assert.assertEquals(ctx.terminated().size(), 1); validateAddEventResult(resource, input, Response.Status.OK); Assert.assertEquals(ctx.selectedOn().size(), 2); Assert.assertEquals(ctx.terminated().size(), 2); // TYPE_C.name4 only has two instances, so the 3rd ondemand termination // will not terminate anything. validateAddEventResult(resource, input, Response.Status.GONE); Assert.assertEquals(ctx.selectedOn().size(), 3); Assert.assertEquals(ctx.terminated().size(), 2); // Try a different type will work input = "{\"eventType\":\"CHAOS_TERMINATION\",\"groupType\":\"TYPE_C\",\"groupName\":\"name5\"}"; validateAddEventResult(resource, input, Response.Status.OK); Assert.assertEquals(ctx.selectedOn().size(), 4); Assert.assertEquals(ctx.terminated().size(), 3); } @Test void testTerminateNowDisabled() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("ondemandTerminationDisabled.properties"); String input = "{\"eventType\":\"CHAOS_TERMINATION\",\"groupType\":\"TYPE_C\",\"groupName\":\"name4\"}"; Assert.assertEquals(ctx.selectedOn().size(), 0); Assert.assertEquals(ctx.terminated().size(), 0); ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.FORBIDDEN); Assert.assertEquals(ctx.selectedOn().size(), 0); Assert.assertEquals(ctx.terminated().size(), 0); } @Test void testTerminateNowBadInput() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("ondemandTermination.properties"); String input = "{\"groupType\":\"TYPE_C\",\"groupName\":\"name4\"}"; ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.BAD_REQUEST); input = "{\"eventType\":\"CHAOS_TERMINATION\",\"groupName\":\"name4\"}"; resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.BAD_REQUEST); input = "{\"eventType\":\"CHAOS_TERMINATION\",\"groupType\":\"TYPE_C\"}"; resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.BAD_REQUEST); } @Test void testTerminateNowBadGroupNotExist() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("ondemandTermination.properties"); String input = "{\"eventType\":\"CHAOS_TERMINATION\",\"groupType\":\"INVALID\",\"groupName\":\"name4\"}"; ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.NOT_FOUND); input = "{\"eventType\":\"CHAOS_TERMINATION\",\"groupType\":\"TYPE_C\",\"groupName\":\"INVALID\"}"; resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.NOT_FOUND); } @Test void testTerminateNowBadEventType() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("ondemandTermination.properties"); String input = "{\"eventType\":\"INVALID\",\"groupType\":\"TYPE_C\",\"groupName\":\"name4\"}"; ChaosMonkeyResource resource = new ChaosMonkeyResource(new BasicChaosMonkey(ctx)); validateAddEventResult(resource, input, Response.Status.BAD_REQUEST); } @Test public void testResource() { MonkeyRunner.getInstance().replaceMonkey(BasicChaosMonkey.class, MockTestChaosMonkeyContext.class); ChaosMonkeyResource resource = new ChaosMonkeyResource(); MultivaluedMap queryParams = new MultivaluedMapImpl(); queryParams.add("groupType", "ASG"); Date queryDate = new Date(); queryParams.add("since", String.valueOf(queryDate.getTime())); when(mockUriInfo.getQueryParameters()).thenReturn(queryParams); @SuppressWarnings("unchecked") // fix when Matcher.anyMapOf is available Map anyMap = anyMap(); when(mockRecorder.findEvents(any(MonkeyType.class), any(EventType.class), anyMap, any(Date.class))).thenReturn( Arrays.asList(mkEvent("i-123456789012345670"), mkEvent("i-123456789012345671"))); try { Response resp = resource.getChaosEvents(mockUriInfo); Assert.assertEquals(resp.getEntity().toString(), getResource("getChaosEventsResponse.json")); } catch (Exception e) { LOGGER.error("exception from getChaosEvents", e); Assert.fail("getChaosEvents throws exception"); } verify(mockRecorder).findEvents(monkeyTypeArg.capture(), eventTypeArg.capture(), queryArg.capture(), dateArg.capture()); Assert.assertEquals(monkeyTypeArg.getValue(), ChaosMonkey.Type.CHAOS); Assert.assertEquals(eventTypeArg.getValue(), ChaosMonkey.EventTypes.CHAOS_TERMINATION); Map query = queryArg.getValue(); Assert.assertEquals(query.size(), 1); Assert.assertEquals(query.get("groupType"), "ASG"); Assert.assertEquals(dateArg.getValue(), queryDate); } private MonkeyRecorder.Event mkEvent(String instance) { final MonkeyType monkeyType = ChaosMonkey.Type.CHAOS; final EventType eventType = ChaosMonkey.EventTypes.CHAOS_TERMINATION; // SUPPRESS CHECKSTYLE MagicNumber return new BasicRecorderEvent(monkeyType, eventType, "region", instance, 1330538400000L) .addField("groupType", "ASG").addField("groupName", "testGroup"); } public static class MockTestChaosMonkeyContext extends TestChaosMonkeyContext { @Override public MonkeyRecorder recorder() { return mockRecorder; } } String getResource(String name) { // get resource as stream, use Scanner to read stream as one token return new Scanner(TestChaosMonkeyResource.class.getResourceAsStream(name), "UTF-8").useDelimiter("\\A").next(); } private void validateAddEventResult(ChaosMonkeyResource resource, String input, Response.Status responseStatus) { try { Response resp = resource.addEvent(input); Assert.assertEquals(resp.getStatus(), responseStatus.getStatusCode()); } catch (Exception e) { LOGGER.error("exception from addEvent", e); Assert.fail("addEvent throws exception"); } } } ================================================ FILE: src/test/java/com/netflix/simianarmy/tunable/TestTunablyAggressiveChaosMonkey.java ================================================ /* * * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.simianarmy.tunable; import com.amazonaws.services.autoscaling.model.TagDescription; import org.testng.Assert; import org.testng.annotations.Test; import com.netflix.simianarmy.GroupType; import com.netflix.simianarmy.basic.chaos.BasicInstanceGroup; import com.netflix.simianarmy.chaos.ChaosCrawler.InstanceGroup; import com.netflix.simianarmy.chaos.TestChaosMonkeyContext; import java.util.Collections; public class TestTunablyAggressiveChaosMonkey { private enum GroupTypes implements GroupType { TYPE_A, TYPE_B }; @Test public void testFullProbability_basic() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("fullProbability.properties"); TunablyAggressiveChaosMonkey chaos = new TunablyAggressiveChaosMonkey(ctx); InstanceGroup basic = new BasicInstanceGroup("basic", GroupTypes.TYPE_A, "region", Collections.emptyList()); double probability = chaos.getEffectiveProbability(basic); Assert.assertEquals(probability, 1.0); } @Test public void testFullProbability_tuned() { TestChaosMonkeyContext ctx = new TestChaosMonkeyContext("fullProbability.properties"); TunablyAggressiveChaosMonkey chaos = new TunablyAggressiveChaosMonkey(ctx); TunableInstanceGroup tuned = new TunableInstanceGroup("basic", GroupTypes.TYPE_A, "region", Collections.emptyList()); tuned.setAggressionCoefficient(0.5); double probability = chaos.getEffectiveProbability(tuned); Assert.assertEquals(probability, 0.5); } } ================================================ FILE: src/test/resources/chaos.properties ================================================ simianarmy.chaos.enabled = false ================================================ FILE: src/test/resources/client.properties ================================================ simianarmy.client.aws.accountKey = fakeAccount simianarmy.client.aws.secretKey = fakeSecret simianarmy.client.aws.assumeRoleArn = arn:aws:iam::fakeAccount:role/fakeRole simianarmy.client.aws.accountName = default ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/all.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true simianarmy.chaos.TYPE_B.enabled = true ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/cloudformation.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_D.new-group-TestGroup1.enabled = true simianarmy.chaos.TYPE_D.new-group-TestGroup1.probability = 1.0 simianarmy.chaos.TYPE_D.new-group-TestGroup1.maxTerminationsPerDay = 100 simianarmy.chaos.TYPE_D.new-group-TestGroup1.lastOptInTimeInMilliseconds=2000 simianarmy.chaos.TYPE_D.new-group-TestGroup1.notification.enabled = true simianarmy.chaos.TYPE_D.new-group-TestGroup1.ownerEmail = foo@bar.com ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/disabled.properties ================================================ simianarmy.chaos.enabled = false ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/enabledA.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.TYPE_A.enabled = true ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/enabledAwith0.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = false simianarmy.chaos.TYPE_A.name0.enabled = true ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/enabledAwithout1.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true simianarmy.chaos.TYPE_A.name1.enabled = false ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/enabledB.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.TYPE_A.enabled = false simianarmy.chaos.TYPE_B.enabled = true ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/fullProbability.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true simianarmy.chaos.TYPE_A.probability = 1.0 simianarmy.chaos.TYPE_B.enabled = true simianarmy.chaos.TYPE_B.probability = 1.0 simianarmy.scheduler.frequency = 1 simianarmy.scheduler.frequencyUnit = DAYS ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/globalNotificationEnabled.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true simianarmy.chaos.TYPE_B.enabled = true simianarmy.chaos.TYPE_A.name0.notification.enabled = false simianarmy.chaos.TYPE_A.name1.notification.enabled = true simianarmy.chaos.notification.global.enabled = true simianarmy.chaos.notification.global.receiverEmail = test@email.com ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationDisabled.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 0 # Set the last opt-in time to be a pretty distant past simianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999 simianarmy.chaos.mandatoryTermination.enabled = false simianarmy.chaos.mandatoryTermination.windowInDays = 10 simianarmy.chaos.mandatoryTermination.defaultProbability = 1.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationInsideWindow.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.terminateOndemand.enabled = true simianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 10 simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 0 # Set the last opt-in time to be a pretty distant past simianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999 simianarmy.chaos.mandatoryTermination.enabled = true # The window size is big enough so the opt-in time is inside the window simianarmy.chaos.mandatoryTermination.windowInDays = 100000 simianarmy.chaos.mandatoryTermination.defaultProbability = 1.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationNoOptInTime.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 0 # No last opt-in time is specified #simianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999 simianarmy.chaos.mandatoryTermination.enabled = true simianarmy.chaos.mandatoryTermination.windowInDays = 10 simianarmy.chaos.mandatoryTermination.defaultProbability = 1.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationNotDefined.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 0 # Set the last opt-in time to be a pretty distant past simianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999 # Not having the mandatoryTerminationDisabled.properties is the same # as defining it as false #simianarmy.chaos.mandatoryTermination.enabled = false simianarmy.chaos.mandatoryTermination.windowInDays = 10 simianarmy.chaos.mandatoryTermination.defaultProbability = 1.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/mandatoryTerminationOutsideWindow.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.terminateOndemand.enabled = true simianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 10 simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 0 # Set the last opt-in time to be a pretty distant past simianarmy.chaos.TYPE_C.name4.lastOptInTimeInMilliseconds = 9999 simianarmy.chaos.mandatoryTermination.enabled = true # The window size is small enough so the opt-in time is outside the window simianarmy.chaos.mandatoryTermination.windowInDays = 10 simianarmy.chaos.mandatoryTermination.defaultProbability = 1.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/noProbability.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true simianarmy.chaos.TYPE_A.probability = 0.0 simianarmy.chaos.TYPE_B.enabled = true simianarmy.chaos.TYPE_B.probability = 0.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/noProbabilityByName.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true simianarmy.chaos.TYPE_A.probability = 1.0 simianarmy.chaos.TYPE_A.name0.probability = 0.0 simianarmy.chaos.TYPE_A.name1.probability = 0.0 simianarmy.chaos.TYPE_B.enabled = true simianarmy.chaos.TYPE_B.probability = 1.0 simianarmy.chaos.TYPE_B.name2.probability = 0.0 simianarmy.chaos.TYPE_B.name3.probability = 0.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/notificationEnabled.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true simianarmy.chaos.TYPE_B.enabled = true simianarmy.chaos.TYPE_A.name0.notification.enabled = true simianarmy.chaos.TYPE_A.name1.notification.enabled = true ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/ondemandTermination.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.terminateOndemand.enabled = true ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/ondemandTerminationDisabled.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.terminateOndemand.enabled = false ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/propertiesWithDefaults.properties ================================================ simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true simianarmy.chaos.TYPE_A.probability = 1.0 simianarmy.chaos.TYPE_A.maxTerminationsPerDay = 2.0 simianarmy.chaos.TYPE_A.named1.enabled = false simianarmy.chaos.TYPE_A.named1.probability = 1.1 simianarmy.chaos.TYPE_A.named1.maxTerminationsPerDay = 2.1 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsBiggerThanOne.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 1.0 simianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 2 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsNegative.properties ================================================ simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 1.0 simianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = -1.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsOne.properties ================================================ simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 1.0 simianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 1.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsSmallerThanOne.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 1.0 simianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 0.5 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsVerySmall.properties ================================================ simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 1.0 simianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 0.0005 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayAsZero.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 1.0 simianarmy.chaos.TYPE_C.name4.maxTerminationsPerDay = 0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/terminationPerDayGroupLevel.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_C.name4.enabled = true simianarmy.chaos.TYPE_C.name4.probability = 1.0 simianarmy.chaos.TYPE_C.maxTerminationsPerDay = 3.0 ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/unleashedEnabledA.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = true ================================================ FILE: src/test/resources/com/netflix/simianarmy/chaos/unleashedEnabledB.properties ================================================ simianarmy.chaos.enabled = true simianarmy.chaos.leashed = false simianarmy.chaos.TYPE_A.enabled = false simianarmy.chaos.TYPE_B.enabled = true ================================================ FILE: src/test/resources/com/netflix/simianarmy/resources/chaos/getChaosEventsResponse.json ================================================ [{"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"}] ================================================ FILE: src/test/resources/log4j.properties ================================================ log4j.rootLogger=ERROR, stdout # stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} - %-5p %C{1} - [%F:%L] %m%n ================================================ FILE: src/test/resources/proxy.properties ================================================ # Proxy configuration for the purpose of testing simianarmy.client.aws.proxyHost=127.0.0.1 simianarmy.client.aws.proxyPort=80 simianarmy.client.aws.proxyUser=fakeUser simianarmy.client.aws.proxyPassword=fakePassword ================================================ FILE: src/test/resources/simianarmy.properties ================================================ simianarmy.chaos.enabled = true simianarmy.calendar.isMonkeyTime = true