├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── ec2-blackout ├── ec2-blackout.gemspec ├── lib ├── ec2-blackout.rb └── ec2-blackout │ ├── auto_scaling_group.rb │ ├── commands.rb │ ├── ec2_instance.rb │ ├── options.rb │ ├── shutdown.rb │ ├── startup.rb │ └── version.rb └── spec ├── ec2-blackout ├── auto_scaling_group_spec.rb ├── ec2_instance_spec.rb ├── options_spec.rb ├── shutdown_spec.rb └── startup_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | vendor/bundle 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ec2-sleeper.gemspec 4 | gemspec 5 | 6 | gem "rspec", "2.14.1" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Stephen Bartlett 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ec2::Blackout 2 | 3 | Want to reduce your EC2 costs? 4 | 5 | Do you stop EC2 instances out of business hours? 6 | 7 | If you don't, `ec2-blackout` could save you money 8 | 9 | `ec2-blackout` is a command-line tool to stop running EC2 instances and Auto Scaling Groups. 10 | 11 | Use ec2-blackout to shutdown EC2 instances when they are idle, for example when you are not in the office. 12 | 13 | If an instance has an Elastic IP address, `ec2-blackout` will reassociate the EIP when the instance is started. 14 | Note: When an instance with an EIP is stopped AWS will automatically disassociate the EIP. AWS charge a small hourly fee for an unattached EIP. 15 | 16 | Instances within Auto Scaling Groups are stopped by setting the group's "desired capacity" to zero, which causes all running instances to be terminated. 17 | 18 | Certinaly not suitable for production instances but development and test instances can generally be shutdown overnight to save money. 19 | 20 | ## Installation 21 | 22 | $ gem install ec2-blackout 23 | 24 | It is recommended you create an access policy using Amazon IAM 25 | 26 | 1. Sign in to your AWS management console and go to the IAM section 27 | 2. Create a group and paste in the following policy 28 | ```json 29 | { 30 | "Statement": [ 31 | { 32 | "Action": [ 33 | "autoscaling:CreateOrUpdateTags", 34 | "autoscaling:DeleteTags", 35 | "autoscaling:DescribeAutoScalingGroups", 36 | "autoscaling:SetDesiredCapacity" 37 | ], 38 | "Effect": "Allow", 39 | "Resource": "*" 40 | }, 41 | { 42 | "Action": [ 43 | "ec2:StartInstances", 44 | "ec2:StopInstances", 45 | "ec2:DescribeTags", 46 | "ec2:CreateTags", 47 | "ec2:DeleteTags", 48 | "ec2:DescribeInstances", 49 | "ec2:DescribeRegions", 50 | "ec2:DescribeAddresses", 51 | "ec2:AssociateAddress" 52 | ], 53 | "Effect": "Allow", 54 | "Resource": "*" 55 | } 56 | ] 57 | } 58 | ``` 59 | 60 | 3. Create a user account and download the access key. 61 | 4. Add the user to the previously created group. 62 | 63 | 64 | Once installed you need to export your AWS credentials 65 | 66 | export AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY 67 | export AWS_SECRET_ACCESS_KEY=YOUR_SECREY_KEY 68 | 69 | ## Stopping Auto Scaling Groups 70 | 71 | EC2 blackout will try to stop instances that are running within auto scaling groups, as well as normal standalone instances. It accomplishes this by setting the "desired capacity" of the auto scaling group to zero, and then restoring it to its previous value when starting up again. It is important to note that you can't actually stop an auto scaled instance - they can only be terminated. 72 | 73 | In order for auto scaled instances to be shut down, the "min size" attribute of the auto scaling group must be set to zero. If it is not, no instances within the group will be shut down. 74 | 75 | ## Usage 76 | 77 | To get help on the commands available: 78 | 79 | $ ec2-blackout --help 80 | 81 | To run a blackout across all AWS regions: 82 | 83 | $ ec2-blackout on 84 | 85 | To run a blackout across a subset of AWS regions: 86 | 87 | $ ec2-blackout on --regions us-east-1,us-west-1 88 | 89 | You can exclude instances from the blackout based on their EC2 tags as well. For instances that belong to an auto scaling group, the tags are matched against the Auto Scaling Group's tags, NOT the tags of the instances themselves. The name of the auto scaling group is treated as if it were a tag with key "Name". Tags are matched using regular expressions. 90 | 91 | # Exclude instances that have a tag with key "no_blackout" 92 | $ ec2-blackout on --exclude-by-tag no_blackout 93 | 94 | # Exclude instances whose "environment" tag "preprod" 95 | $ ec2-blackout on --exclude-by-tag 'environment=preprod' 96 | 97 | # Exclude instances whose "environment" tag is either "preprod" OR "integration" 98 | $ ec2-blackout on --exclude-by-tag 'environment=preprod|integration' 99 | 100 | # Exclude instances whose "Name" tag starts with "myapp". 101 | $ ec2-blackout on --exclude-by-tag 'Name=myapp.*' 102 | 103 | # Exclude instances whose "Name" tag starts with "myapp" AND whose "environment" tag is "preprod" or "integration" 104 | $ ec2-blackout on --exclude-by-tag 'Name=myapp.*,environment=preprod|integration' 105 | 106 | Similarly, you can also specifically *include* instances in the blackout based on their tags. If this option is used, only matching instances will be stopped. The syntax is the same as for exclude tags. 107 | 108 | # Stop only those instances whose "Name" tag starts with "myapp" AND whose "environment" tag is "test" 109 | $ ec2-blackout on --include-by-tag 'Name=myapp.*,environment=test' 110 | 111 | Excludes and includes can be used together if you like: 112 | 113 | # Stop all instances whose name starts with "myapp" except those whose "environment" is "preprod" 114 | $ ec2-blackout on --include-by-tag 'Name=myapp.*' --exclude-by-tag 'environment=preprod' 115 | 116 | To leave a blackout and start the instances that were previously stopped: 117 | 118 | $ ec2-blackout off 119 | 120 | `ec2-blackout` also provides a dry-run using the `--dry-run` option. This option shows you what will be done, but without actually doing it. 121 | 122 | 123 | ## Contributing 124 | 125 | 1. Fork it 126 | 2. Create your feature branch (`git checkout -b my-new-feature`) 127 | 3. Commit your changes (`git commit -am 'Added some feature'`) 128 | 4. Push to the branch (`git push origin my-new-feature`) 129 | 5. Create new Pull Request 130 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | -------------------------------------------------------------------------------- /bin/ec2-blackout: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | begin 4 | require 'ec2-blackout/commands' 5 | rescue LoadError 6 | require 'rubygems' 7 | require 'ec2-blackout/commands' 8 | end 9 | -------------------------------------------------------------------------------- /ec2-blackout.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/ec2-blackout/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Stephen Bartlett", "Charles Blaxland"] 6 | gem.email = ["stephenb@rtlett.org"] 7 | gem.description = Ec2::Blackout.description 8 | gem.summary = Ec2::Blackout.summary 9 | gem.homepage = "https://github.com/srbartlett/ec2-blackout" 10 | gem.add_dependency 'commander' 11 | gem.add_dependency 'aws-sdk-v1', '~> 1.0' 12 | gem.add_dependency 'colorize' 13 | gem.files = `git ls-files`.split($\) 14 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 15 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 16 | gem.name = "ec2-blackout" 17 | gem.require_paths = ["lib"] 18 | gem.version = Ec2::Blackout::VERSION 19 | end 20 | -------------------------------------------------------------------------------- /lib/ec2-blackout.rb: -------------------------------------------------------------------------------- 1 | require "ec2-blackout/version" 2 | require 'commander' 3 | require 'aws-sdk-v1' 4 | require 'ec2-blackout/options' 5 | require 'ec2-blackout/auto_scaling_group' 6 | require 'ec2-blackout/ec2_instance' 7 | require 'ec2-blackout/startup' 8 | require 'ec2-blackout/shutdown' 9 | -------------------------------------------------------------------------------- /lib/ec2-blackout/auto_scaling_group.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | class Ec2::Blackout::AutoScalingGroup 4 | TIMESTAMP_TAG_NAME = 'ec2:blackout:on' 5 | DESIRED_CAPACITY_TAG_NAME = 'ec2:blackout:desired_capacity' 6 | 7 | def self.groups(region, options) 8 | AWS::AutoScaling.new(:region => region).groups.map do |group| 9 | Ec2::Blackout::AutoScalingGroup.new(group, options) 10 | end 11 | end 12 | 13 | def initialize(group, options) 14 | @group, @options = group, options 15 | end 16 | 17 | def stop 18 | tag 19 | zero_desired_capacity 20 | end 21 | 22 | def start 23 | restore_desired_capacity 24 | untag 25 | end 26 | 27 | def stoppable? 28 | AWS.memoize do 29 | if @options.matches_exclude_tags?(tags) 30 | [false, "matches exclude tags"] 31 | elsif !@options.matches_include_tags?(tags) 32 | [false, "does not match include tags"] 33 | elsif @group.desired_capacity == 0 34 | [false, "group has already been stopped"] 35 | elsif @group.min_size > 0 36 | [false, "minimum ASG size is greater than zero - set min_size to 0 if you want to be able to stop this AutoScalingGroup"] 37 | else 38 | true 39 | end 40 | end 41 | end 42 | 43 | def startable? 44 | if @group.max_size == 0 45 | [false, "maximum ASG size is zero"] 46 | elsif @options.force 47 | true 48 | elsif !tags[TIMESTAMP_TAG_NAME] 49 | [false, "instance was not originally stopped by ec2-blackout"] 50 | else 51 | true 52 | end 53 | end 54 | 55 | def to_s 56 | "autoscaling group #{@group.name}" 57 | end 58 | 59 | 60 | private 61 | 62 | def tag 63 | @group.update(:tags => [ 64 | { :key => TIMESTAMP_TAG_NAME, :value => Time.now.utc.to_s, :propagate_at_launch => false }, 65 | { :key => DESIRED_CAPACITY_TAG_NAME, :value => @group.desired_capacity.to_s, :propagate_at_launch => false } 66 | ]) 67 | end 68 | 69 | def untag 70 | @group.delete_tags([ 71 | { :key => TIMESTAMP_TAG_NAME, :value => tags[TIMESTAMP_TAG_NAME], :propagate_at_launch => false }, 72 | { :key => DESIRED_CAPACITY_TAG_NAME, :value => tags[DESIRED_CAPACITY_TAG_NAME], :propagate_at_launch => false } 73 | ]) 74 | end 75 | 76 | def zero_desired_capacity 77 | @group.set_desired_capacity(0) 78 | end 79 | 80 | def restore_desired_capacity 81 | previous_desired_capacity = tags[DESIRED_CAPACITY_TAG_NAME].to_i 82 | previous_desired_capacity = @group.min_size if previous_desired_capacity == 0 83 | @group.set_desired_capacity(previous_desired_capacity) 84 | end 85 | 86 | def tags 87 | @tags ||= begin 88 | tags_array = @group.tags.map { |tag| [tag[:key], tag[:value]] } 89 | tags_hash = Hash[tags_array] 90 | {"Name" => @group.name}.merge(tags_hash) 91 | end 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /lib/ec2-blackout/commands.rb: -------------------------------------------------------------------------------- 1 | require 'ec2-blackout' 2 | require 'commander/import' 3 | 4 | program :name, Ec2::Blackout.name 5 | program :version, Ec2::Blackout::VERSION 6 | program :description, Ec2::Blackout.summary 7 | 8 | default_command :help 9 | 10 | command "on" do |c| 11 | c.syntax = '[options]' 12 | c.summary = 'Shutdown EC2 instances' 13 | c.description = 'For each AWS Region, find running EC2 instances and shut them down.' 14 | 15 | c.option '-r', '--regions region1,region2', Array, 'Comma separated list of regions to search. Defaults to all regions' 16 | c.option '-x', '--exclude-by-tag tagname1=value,tagname2', Array, 'Comma separated list of name-value tags to exclude from the blackout' 17 | c.option '-i', '--include-by-tag tagname1=value,tagname2', Array, 'Comma separated list of name-value tags to include in the blackout' 18 | c.option '-d', '--dry-run', 'Find instances without stopping them' 19 | 20 | c.action do |args, options| 21 | Ec2::Blackout::Shutdown.new(self, Ec2::Blackout::Options.new(options.__hash__)).execute 22 | end 23 | end 24 | 25 | command "off" do |c| 26 | c.syntax = '[options]' 27 | c.summary = 'Start up EC2 instances' 28 | c.description = 'For each AWS Region, find EC2 instances previously shutdown by ec2-blackout and start them up.' 29 | 30 | c.option '-r', '--regions region1,region2', Array, 'Comma separated list of regions to search. Defaults to all regions' 31 | c.option '-f', '--force', 'Force start up regardless of who shut them dowm' 32 | c.option '-x', '--exclude-by-tag tagname1=value,tagname2', Array, 'Comma separated list of name-value tags to exclude from the blackout off' 33 | c.option '-i', '--include-by-tag tagname1=value,tagname2', Array, 'Comma separated list of name-value tags to include in the blackout off' 34 | c.option '-d', '--dry-run', 'Find instances without starting them' 35 | 36 | c.action do |args, options| 37 | Ec2::Blackout::Startup.new(self, Ec2::Blackout::Options.new(options.__hash__)).execute 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/ec2-blackout/ec2_instance.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | 3 | class Ec2::Blackout::Ec2Instance 4 | 5 | TIMESTAMP_TAG_NAME = 'ec2:blackout:on' 6 | EIP_TAG_NAME = 'ec2:blackout:eip' 7 | 8 | def self.running_instances(region, options) 9 | instances(region, options, 'running') 10 | end 11 | 12 | def self.stopped_instances(region, options) 13 | instances(region, options, 'stopped') 14 | end 15 | 16 | 17 | attr_accessor :eip_retry_delay_seconds 18 | 19 | def initialize(instance, options) 20 | @instance, @options = instance, options 21 | @eip_retry_delay_seconds = 5 22 | end 23 | 24 | def stop 25 | tag 26 | @instance.stop 27 | end 28 | 29 | def start 30 | @instance.start 31 | associate_eip 32 | untag 33 | end 34 | 35 | def stoppable? 36 | if tags['aws:autoscaling:groupName'] 37 | [false, "instance is part of an autoscaling group"] 38 | elsif @instance.status != :running 39 | [false, "instance is not in running state"] 40 | elsif @options.matches_exclude_tags?(tags) 41 | [false, "matches exclude tags"] 42 | elsif !@options.matches_include_tags?(tags) 43 | [false, "does not match include tags"] 44 | elsif @instance.root_device_type != :ebs 45 | [false, "is not ebs root device"] 46 | else 47 | true 48 | end 49 | end 50 | 51 | def startable? 52 | if @instance.status != :stopped 53 | [false, "instance is not in stopped state"] 54 | elsif @options.force 55 | true 56 | elsif !tags[TIMESTAMP_TAG_NAME] 57 | [false, "instance was not originally stopped by ec2-blackout"] 58 | elsif @options.matches_exclude_tags?(tags) 59 | [false, "matches exclude tags"] 60 | elsif !@options.matches_include_tags?(tags) 61 | [false, "does not match include tags"] 62 | else 63 | true 64 | end 65 | end 66 | 67 | def to_s 68 | s = "instance #{@instance.id}" 69 | s += " (#{tags['Name']})" if tags['Name'] 70 | s 71 | end 72 | 73 | 74 | private 75 | 76 | def self.instances(region, options, instance_status) 77 | AWS::EC2.new(:region => region).instances.filter('instance-state-name', instance_status).map do |instance| 78 | Ec2::Blackout::Ec2Instance.new(instance, options) 79 | end 80 | end 81 | 82 | 83 | def tag 84 | @instance.add_tag(TIMESTAMP_TAG_NAME, :value => Time.now.utc) 85 | if @instance.has_elastic_ip? 86 | if @instance.elastic_ip.vpc? 87 | @instance.add_tag(EIP_TAG_NAME, :value => @instance.elastic_ip.allocation_id) 88 | else 89 | @instance.add_tag(EIP_TAG_NAME, :value => @instance.elastic_ip.public_ip) 90 | end 91 | end 92 | end 93 | 94 | def untag 95 | @instance.tags.delete(TIMESTAMP_TAG_NAME) 96 | @instance.tags.delete(EIP_TAG_NAME) 97 | end 98 | 99 | def tags 100 | @tags ||= @instance.tags.to_h 101 | end 102 | 103 | def associate_eip 104 | eip = @instance.tags[EIP_TAG_NAME] 105 | if eip 106 | attempts = 0 107 | begin 108 | @instance.associate_elastic_ip(eip) 109 | rescue 110 | sleep eip_retry_delay_seconds 111 | attempts += 1 112 | retry if attempts * eip_retry_delay_seconds < 5*60 # 5 mins 113 | end 114 | end 115 | end 116 | 117 | end 118 | -------------------------------------------------------------------------------- /lib/ec2-blackout/options.rb: -------------------------------------------------------------------------------- 1 | 2 | class Ec2::Blackout::Options 3 | 4 | DEFAULT_REGIONS = ['us-east-1', 'us-west-1', 'us-west-2', 'eu-west-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'sa-east-1'] 5 | 6 | def initialize(options = {}) 7 | @options = options 8 | @options[:regions] = DEFAULT_REGIONS unless @options[:regions] 9 | end 10 | 11 | def include_tags 12 | @include_tags ||= key_value_hash(@options[:include_by_tag]) 13 | end 14 | 15 | def exclude_tags 16 | @exclude_tags ||= key_value_hash(@options[:exclude_by_tag]) 17 | end 18 | 19 | def matches_include_tags?(tags) 20 | return true unless include_tags 21 | matches_tags?(include_tags, tags) 22 | end 23 | 24 | def matches_exclude_tags?(tags) 25 | return false unless exclude_tags 26 | matches_tags?(exclude_tags, tags) 27 | end 28 | 29 | def regions 30 | @options[:regions] 31 | end 32 | 33 | def dry_run 34 | @options[:dry_run] 35 | end 36 | 37 | def force 38 | @options[:force] 39 | end 40 | 41 | 42 | private 43 | 44 | def matches_tags?(tags_to_match, tags) 45 | tags_to_match.each do |tag_name, tag_value| 46 | return false unless tags[tag_name] =~ /#{tag_value}/ 47 | end 48 | true 49 | end 50 | 51 | def key_value_hash(options) 52 | options ? Hash[options.map { |opt| opt.split("=", 2).map(&:strip) }] : nil 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /lib/ec2-blackout/shutdown.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | class Ec2::Blackout::Shutdown 4 | 5 | def initialize(ui, options) 6 | @ui, @options = ui, options 7 | end 8 | 9 | def execute 10 | @ui.say 'Dry run specified - no instances will be stopped'.bold if @options.dry_run 11 | @ui.say "Stopping instances" 12 | @options.regions.each do |region| 13 | @ui.say "Checking region #{region}" 14 | shutdown(Ec2::Blackout::AutoScalingGroup.groups(region, @options)) 15 | shutdown(Ec2::Blackout::Ec2Instance.running_instances(region, @options)) 16 | end 17 | @ui.say 'Done!' 18 | end 19 | 20 | private 21 | 22 | def shutdown(resources) 23 | resources.each do |resource| 24 | stoppable, reason = resource.stoppable? 25 | if stoppable 26 | @ui.say "-> Stopping #{resource}".yellow 27 | resource.stop unless @options.dry_run 28 | else 29 | @ui.say "-> Skipping #{resource}: #{reason}" 30 | end 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /lib/ec2-blackout/startup.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | class Ec2::Blackout::Startup 4 | 5 | def initialize(ui, options) 6 | @ui, @options = ui, options 7 | end 8 | 9 | def execute 10 | @ui.say 'Dry run specified - no instances will be started'.bold if @options.dry_run 11 | @ui.say "Starting instances" 12 | @options.regions.each do |region| 13 | @ui.say "Checking region #{region}" 14 | startup(Ec2::Blackout::AutoScalingGroup.groups(region, @options)) 15 | startup(Ec2::Blackout::Ec2Instance.stopped_instances(region, @options)) 16 | end 17 | @ui.say 'Done!' 18 | end 19 | 20 | private 21 | 22 | def startup(resources) 23 | resources.each do |resource| 24 | startable, reason = resource.startable? 25 | if startable 26 | @ui.say "-> Starting #{resource}".green 27 | resource.start unless @options.dry_run 28 | else 29 | @ui.say "-> Skipping #{resource}: #{reason}" 30 | end 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /lib/ec2-blackout/version.rb: -------------------------------------------------------------------------------- 1 | module Ec2 2 | module Blackout 3 | VERSION = "0.0.10" 4 | 5 | def self.name 6 | 'ec2-blackout' 7 | end 8 | 9 | def self.summary 10 | 'A command-line tool to shutdown EC2 instances.' 11 | end 12 | 13 | def self.description 14 | %q{ 15 | ec2-blackout is a command line tool to shutdown EC2 instances. 16 | 17 | It's main purpose is to save you money by stopping EC2 instances during 18 | out of office hours. 19 | } 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/ec2-blackout/auto_scaling_group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Ec2::Blackout 4 | describe AutoScalingGroup do 5 | 6 | describe ".groups" do 7 | 8 | it "returns all groups in the region" do 9 | auto_scaling_stub = double("autoscaling") 10 | auto_scaling_stub.should_receive(:groups).and_return([double, double]) 11 | AWS::AutoScaling.should_receive(:new).with(:region => "ap-southeast-2").and_return(auto_scaling_stub) 12 | groups = AutoScalingGroup.groups("ap-southeast-2", Options.new) 13 | expect(groups.size).to eq 2 14 | end 15 | 16 | end 17 | 18 | 19 | describe "#stop" do 20 | let!(:aws_group) { double("autoscaling group") } 21 | let!(:group) { stubbed_stoppable_auto_scaling_group(aws_group) } 22 | 23 | it "sets desired capacity to zero" do 24 | aws_group.should_receive(:set_desired_capacity).with(0) 25 | group.stop 26 | end 27 | 28 | it "tags the instance with timestamp and original desired capacity" do 29 | aws_group.stub(:desired_capacity).and_return(3) 30 | aws_group.should_receive(:update).with do |attributes| 31 | expect_ec2_blackout_tags(attributes[:tags], Time.now, 3) 32 | end 33 | 34 | group.stop 35 | end 36 | 37 | end 38 | 39 | 40 | describe "#start" do 41 | let!(:aws_group) { double("autoscaling group") } 42 | let!(:group) { stubbed_startable_auto_scaling_group(aws_group) } 43 | 44 | it "restores desired capacity to its previous setting" do 45 | aws_group.should_receive(:tags).and_return(ec2_blackout_tags("2014-02-10 11:44:58 UTC", 3)) 46 | aws_group.should_receive(:set_desired_capacity).with(3) 47 | group.start 48 | end 49 | 50 | it "removes the ec2 blackout tags from the autoscaling group" do 51 | tags = ec2_blackout_tags("2014-02-10 12s:44:58 UTC", 2) 52 | aws_group.stub(:tags).and_return(tags) 53 | aws_group.should_receive(:delete_tags).with(tags) 54 | group.start 55 | end 56 | 57 | it "sets desired capacity to min capacity if there is no desired capacity tag" do 58 | tags = ec2_blackout_tags("2014-02-10 11:44:58 UTC", nil) 59 | aws_group.stub(:tags).and_return(tags) 60 | aws_group.stub(:min_size).and_return(4) 61 | aws_group.should_receive(:set_desired_capacity).with(4) 62 | group.start 63 | end 64 | 65 | end 66 | 67 | 68 | describe("#stoppable?") do 69 | 70 | let!(:aws_group) { double("autoscaling group") } 71 | let!(:group) { stubbed_stoppable_auto_scaling_group(aws_group) } 72 | 73 | it "returns true if the group meets all the stoppable conditions" do 74 | stoppable, reason = group.stoppable? 75 | expect(stoppable).to be_true 76 | end 77 | 78 | it "returns false if the tags match the exclude tag options" do 79 | options = Ec2::Blackout::Options.new(:exclude_by_tag => ["foo=bar"]) 80 | group = stubbed_stoppable_auto_scaling_group(aws_group, options) 81 | aws_group.stub(:tags).and_return(asg_tags("foo" => "bar")) 82 | 83 | stoppable, reason = group.stoppable? 84 | expect(stoppable).to be_false 85 | end 86 | 87 | it "returns false if the tags do not match the include tag options" do 88 | options = Ec2::Blackout::Options.new(:include_by_tag => ["foo=bar"]) 89 | group = stubbed_stoppable_auto_scaling_group(aws_group, options) 90 | aws_group.stub(:tags).and_return(asg_tags("foo" => "baz")) 91 | 92 | stoppable, reason = group.stoppable? 93 | expect(stoppable).to be_false 94 | end 95 | 96 | it "returns false if the desired capacity is zero" do 97 | aws_group.stub(:desired_capacity).and_return(0) 98 | stoppable, reason = group.stoppable? 99 | expect(stoppable).to be_false 100 | end 101 | 102 | it "returns false if min size is greater than zero" do 103 | aws_group.stub(:min_size).and_return(1) 104 | stoppable, reason = group.stoppable? 105 | expect(stoppable).to be_false 106 | end 107 | 108 | it "treats the name of the ASG as a tag with key 'Name'" do 109 | options = Ec2::Blackout::Options.new(:include_by_tag => ["Name=My ASG"]) 110 | group = stubbed_stoppable_auto_scaling_group(aws_group, options) 111 | aws_group.stub(:name).and_return("My ASG") 112 | 113 | stoppable, reason = group.stoppable? 114 | expect(stoppable).to be_true 115 | end 116 | 117 | end 118 | 119 | 120 | describe("#startable?") do 121 | 122 | let!(:aws_group) { double("autoscaling group") } 123 | let!(:group) { stubbed_startable_auto_scaling_group(aws_group) } 124 | 125 | it "returns true if the instance was previously stopped with ec2 blackout" do 126 | startable, reason = group.startable? 127 | expect(startable).to be_true 128 | end 129 | 130 | it "returns false if there is no ec2 blackout timestamp tag" do 131 | aws_group.stub(:tags).and_return([]) 132 | startable, reason = group.startable? 133 | expect(startable).to be_false 134 | end 135 | 136 | it "returns true if the force option was specified, even if there is no ec2 blackout timestamp tag" do 137 | options = Ec2::Blackout::Options.new(:force => true) 138 | aws_group = double("autoscaling group") 139 | group = stubbed_startable_auto_scaling_group(aws_group, options) 140 | aws_group.stub(:tags).and_return([]) 141 | startable, reason = group.startable? 142 | expect(startable).to be_true 143 | end 144 | 145 | it "returns false if max size is zero" do 146 | aws_group.stub(:max_size).and_return(0) 147 | startable, reason = group.startable? 148 | expect(startable).to be_false 149 | end 150 | 151 | end 152 | 153 | 154 | def stubbed_stoppable_auto_scaling_group(underlying_aws_stub, options = Options.new) 155 | underlying_aws_stub.stub(:name).and_return("Test AutoScaling Group") 156 | underlying_aws_stub.stub(:desired_capacity).and_return(1) 157 | underlying_aws_stub.stub(:min_size).and_return(0) 158 | underlying_aws_stub.stub(:tags).and_return([]) 159 | underlying_aws_stub.stub(:update) 160 | underlying_aws_stub.stub(:set_desired_capacity) 161 | AutoScalingGroup.new(underlying_aws_stub, options) 162 | end 163 | 164 | def stubbed_startable_auto_scaling_group(underlying_aws_stub, options = Options.new) 165 | underlying_aws_stub.stub(:name).and_return("Test AutoScaling Group") 166 | underlying_aws_stub.stub(:tags).and_return(ec2_blackout_tags("2014-02-10 11:44:58 UTC", 1)) 167 | underlying_aws_stub.stub(:max_size).and_return(10) 168 | underlying_aws_stub.stub(:delete_tags) 169 | underlying_aws_stub.stub(:set_desired_capacity) 170 | AutoScalingGroup.new(underlying_aws_stub, options) 171 | end 172 | 173 | def ec2_blackout_tags(timestamp, desired_capacity) 174 | tags = asg_tags(AutoScalingGroup::TIMESTAMP_TAG_NAME => timestamp) 175 | if desired_capacity 176 | tags += asg_tags(AutoScalingGroup::DESIRED_CAPACITY_TAG_NAME => desired_capacity.to_s) 177 | end 178 | tags 179 | end 180 | 181 | def asg_tags(tags_hash) 182 | tags_hash.map {|k,v| {key: k, value: v, propagate_at_launch: false} } 183 | end 184 | 185 | def expect_ec2_blackout_tags(tags, timestamp, desired_capacity) 186 | timestamp_tag = tags.find { |tag| tag[:key] == AutoScalingGroup::TIMESTAMP_TAG_NAME } 187 | expect(Date.parse(timestamp_tag[:value]).to_time - timestamp).to be < 1 188 | expect(timestamp_tag[:propagate_at_launch]).to be_false 189 | 190 | desired_capacity_tag = tags.find { |tag| tag[:key] == AutoScalingGroup::DESIRED_CAPACITY_TAG_NAME } 191 | expect(desired_capacity_tag[:value]).to eq desired_capacity.to_s 192 | expect(desired_capacity_tag[:propagate_at_launch]).to be_false 193 | end 194 | 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /spec/ec2-blackout/ec2_instance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Ec2::Blackout 4 | 5 | describe Ec2Instance do 6 | 7 | describe ".running_instances" do 8 | it "returns a list of instances that are in the running state" do 9 | expect_instance_filter('running', 'ap-southeast-2') 10 | Ec2Instance.running_instances("ap-southeast-2", Options.new) 11 | end 12 | end 13 | 14 | describe ".stopped_instances" do 15 | it "returns a list of instances that are in the stopped state" do 16 | expect_instance_filter('stopped', 'ap-southeast-2') 17 | Ec2Instance.stopped_instances("ap-southeast-2", Options.new) 18 | end 19 | end 20 | 21 | describe "#stop" do 22 | let!(:aws_instance) { double("ec2 instance") } 23 | let!(:instance) { stubbed_stoppable_instance(aws_instance) } 24 | 25 | it "stops the instance" do 26 | aws_instance.should_receive(:stop) 27 | instance.stop 28 | end 29 | 30 | it "tags the instance with a timestamp indicating when it was stopped" do 31 | aws_instance.should_receive(:add_tag).with do |tag_name, tag_attributes| 32 | expect(tag_name).to eq Ec2Instance::TIMESTAMP_TAG_NAME 33 | expect(Time.now - tag_attributes[:value]).to be < 1 34 | end 35 | 36 | instance.stop 37 | end 38 | 39 | context 'when the instance has an Elastic IP' do 40 | let(:eip) { double(:eip) } 41 | 42 | before do 43 | aws_instance.stub(:has_elastic_ip?).and_return(true) 44 | aws_instance.stub(:elastic_ip).and_return(eip) 45 | end 46 | 47 | context 'and it is running in a VPC' do 48 | 49 | before do 50 | eip.stub(:vpc?).and_return(true) 51 | end 52 | 53 | it "saves the associated elastic IP as a tag" do 54 | eip.should_receive(:allocation_id).and_return("eipalloc-eab23c0d") 55 | aws_instance.should_receive(:add_tag).with(Ec2Instance::EIP_TAG_NAME, :value => "eipalloc-eab23c0d") 56 | instance.stop 57 | end 58 | end 59 | 60 | context 'and it is running in EC2 classic' do 61 | before do 62 | eip.stub(:vpc?).and_return(false) 63 | end 64 | 65 | it "saves the elastic public IP as a tag" do 66 | eip.should_receive(:public_ip).and_return("public-ip") 67 | aws_instance.should_receive(:add_tag).with(Ec2Instance::EIP_TAG_NAME, :value => "public-ip") 68 | instance.stop 69 | end 70 | end 71 | end 72 | 73 | end 74 | 75 | describe "#start" do 76 | let!(:aws_instance) { double("ec2 instance") } 77 | let!(:instance) { stubbed_startable_instance(aws_instance) } 78 | 79 | it "starts the instance" do 80 | aws_instance.should_receive(:start) 81 | instance.start 82 | end 83 | 84 | it "remove ec2 blackout tags from the instance" do 85 | tags = ec2_tags(Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-12 02:35:52 UTC', Ec2Instance::EIP_TAG_NAME => 'eipalloc-eab23c0d') 86 | aws_instance.stub(:tags).and_return(tags) 87 | tags.should_receive(:delete).with(Ec2Instance::TIMESTAMP_TAG_NAME) 88 | tags.should_receive(:delete).with(Ec2Instance::EIP_TAG_NAME) 89 | instance.start 90 | end 91 | 92 | it "reattaches the previous elastic IP" do 93 | tags = ec2_tags(Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-12 02:35:52 UTC', Ec2Instance::EIP_TAG_NAME => 'eipalloc-eab23c0d') 94 | aws_instance.stub(:tags).and_return(tags) 95 | aws_instance.should_receive(:associate_elastic_ip).with("eipalloc-eab23c0d") 96 | instance.start 97 | end 98 | 99 | it "should retry attaching the elastic IP if it fails" do 100 | tags = ec2_tags(Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-12 02:35:52 UTC', Ec2Instance::EIP_TAG_NAME => 'eipalloc-eab23c0d') 101 | aws_instance.stub(:tags).and_return(tags) 102 | aws_instance.should_receive(:associate_elastic_ip).and_raise(:error) 103 | aws_instance.should_receive(:associate_elastic_ip) 104 | instance.eip_retry_delay_seconds = 0 105 | instance.start 106 | end 107 | end 108 | 109 | describe "#stoppable?" do 110 | let!(:aws_instance) { double("ec2 instance") } 111 | let!(:instance) { stubbed_stoppable_instance(aws_instance) } 112 | 113 | it "returns true if the instance meets all conditions for being stoppable" do 114 | stoppable, reason = instance.stoppable? 115 | expect(stoppable).to be_true 116 | end 117 | 118 | it "returns false if the instance is not running" do 119 | aws_instance.stub(:status).and_return(:stopped) 120 | stoppable, reason = instance.stoppable? 121 | expect(stoppable).to be_false 122 | end 123 | 124 | it "returns false if the instance belongs to an autoscaling group" do 125 | aws_instance.stub(:tags).and_return(ec2_tags('aws:autoscaling:groupName' => 'foobar')) 126 | stoppable, reason = instance.stoppable? 127 | expect(stoppable).to be_false 128 | end 129 | 130 | it "returns false if the instance matches the exclude tags specified in the options" do 131 | options = Ec2::Blackout::Options.new(:exclude_by_tag => ["foo=bar"]) 132 | instance = stubbed_stoppable_instance(aws_instance, options) 133 | aws_instance.stub(:tags).and_return(ec2_tags("foo" => "bar")) 134 | stoppable, reason = instance.stoppable? 135 | expect(stoppable).to be_false 136 | end 137 | 138 | it "returns false if the instance does not match the include tags specified in the options" do 139 | options = Ec2::Blackout::Options.new(:include_by_tag => ["foo=bar"]) 140 | instance = stubbed_stoppable_instance(aws_instance, options) 141 | aws_instance.stub(:tags).and_return(ec2_tags("foo" => "baz")) 142 | stoppable, reason = instance.stoppable? 143 | expect(stoppable).to be_false 144 | end 145 | 146 | it "returns false if the instance volume type is not EBS" do 147 | aws_instance.stub(:root_device_type).and_return('not_ebs') 148 | stoppable, reason = instance.stoppable? 149 | expect(stoppable).to be_false 150 | end 151 | end 152 | 153 | describe "#startable?" do 154 | let!(:aws_instance) { double("ec2 instance") } 155 | let!(:instance) { stubbed_startable_instance(aws_instance) } 156 | 157 | it "returns true if the instance meets all conditions for being startable" do 158 | startable, reason = instance.startable? 159 | expect(startable).to be_true 160 | end 161 | 162 | it "returns false if the instance does not have the ec2 blackout timestamp tag" do 163 | aws_instance.stub(:tags).and_return(ec2_tags({})) 164 | startable, reason = instance.startable? 165 | expect(startable).to be_false 166 | end 167 | 168 | it "returns true if the force tag was specified even if the instance does not have the ec2 blackout timestamp tag" do 169 | options = Ec2::Blackout::Options.new(:force => true) 170 | instance = stubbed_startable_instance(aws_instance, options) 171 | aws_instance.stub(:tags).and_return(ec2_tags({})) 172 | startable, reason = instance.startable? 173 | expect(startable).to be_true 174 | end 175 | 176 | it "returns false if the instance matches the exclude tags specified in the options" do 177 | options = Ec2::Blackout::Options.new(:exclude_by_tag => ["foo=bar"]) 178 | instance = stubbed_startable_instance(aws_instance, options) 179 | aws_instance.stub(:tags).and_return(ec2_tags("foo" => "bar", Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-13 02:35:52 UTC')) 180 | startable, reason = instance.startable? 181 | expect(startable).to be_false 182 | end 183 | 184 | it "returns true if the instance matches the include tags specified in the options" do 185 | options = Ec2::Blackout::Options.new(:include_by_tag => ["foo=bar"]) 186 | instance = stubbed_startable_instance(aws_instance, options) 187 | aws_instance.stub(:tags).and_return(ec2_tags("foo" => "bar", Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-13 02:35:52 UTC')) 188 | startable, reason = instance.startable? 189 | expect(startable).to be_true 190 | end 191 | 192 | it "returns false if the instance does not match the include tags specified in the options" do 193 | options = Ec2::Blackout::Options.new(:include_by_tag => ["foo=bar"]) 194 | instance = stubbed_startable_instance(aws_instance, options) 195 | aws_instance.stub(:tags).and_return(ec2_tags("bar" => "baz", Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-13 02:35:52 UTC')) 196 | startable, reason = instance.startable? 197 | expect(startable).to be_false 198 | end 199 | 200 | end 201 | 202 | 203 | def stubbed_stoppable_instance(underlying_aws_stub, options = Options.new) 204 | underlying_aws_stub.stub(:status).and_return(:running) 205 | underlying_aws_stub.stub(:has_elastic_ip?).and_return(false) 206 | underlying_aws_stub.stub(:tags).and_return(ec2_tags({})) 207 | underlying_aws_stub.stub(:add_tag) 208 | underlying_aws_stub.stub(:stop) 209 | underlying_aws_stub.stub(:root_device_type).and_return(:ebs) 210 | Ec2Instance.new(underlying_aws_stub, options) 211 | end 212 | 213 | def stubbed_startable_instance(underlying_aws_stub, options = Options.new) 214 | underlying_aws_stub.stub(:status).and_return(:stopped) 215 | underlying_aws_stub.stub(:has_elastic_ip?).and_return(false) 216 | underlying_aws_stub.stub(:tags).and_return(ec2_tags(Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-13 02:35:52 UTC')) 217 | underlying_aws_stub.stub(:associate_elastic_ip) 218 | underlying_aws_stub.stub(:start) 219 | Ec2Instance.new(underlying_aws_stub, options) 220 | end 221 | 222 | def expect_instance_filter(state, region) 223 | instances_stub = double("instances") 224 | ec2_stub = double("ec2 instance", :instances => instances_stub) 225 | AWS::EC2.should_receive(:new).with(:region => region).and_return(ec2_stub) 226 | instances_stub.should_receive(:filter).with('instance-state-name', state).and_return([double, double]) 227 | end 228 | 229 | def ec2_tags(tags_hash) 230 | # Hack to add a "to_h" method to hash instance (ruby 1.9 doesn't have it, but 2.0 does). 231 | # This lets us use a simple hash instead of an instance of AWS::EC2::ResourceTagCollection to 232 | # represent the instance tags. 233 | if !tags_hash.respond_to?(:to_h) 234 | def tags_hash.to_h 235 | self 236 | end 237 | end 238 | tags_hash 239 | end 240 | 241 | end 242 | 243 | end 244 | -------------------------------------------------------------------------------- /spec/ec2-blackout/options_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ec2::Blackout::Options do 4 | 5 | describe "#exclude_tags" do 6 | 7 | it "converts exclude tags into a hash" do 8 | options = Ec2::Blackout::Options.new :exclude_by_tag => ["Name=foo.*", " Owner = joe ", "Stopped", "Foo="] 9 | expect(options.exclude_tags).to eq({"Name" => "foo.*", "Owner" => "joe", "Stopped" => nil, "Foo" => ""}) 10 | end 11 | 12 | end 13 | 14 | 15 | describe "#matches_exclude_tags" do 16 | 17 | it "matches by regular expression" do 18 | regex_options = Ec2::Blackout::Options.new(:exclude_by_tag => ["Name=foo.*"]) 19 | expect(regex_options.matches_exclude_tags?({"Name" => "foobar"})).to be_true 20 | end 21 | 22 | it "matches by tag name only if no value is given in the options" do 23 | tag_name_only_options = Ec2::Blackout::Options.new(:exclude_by_tag => ["Name"]) 24 | expect(tag_name_only_options.matches_exclude_tags?("Name" => "foobar")).to be_true 25 | expect(tag_name_only_options.matches_exclude_tags?("Owner" => "bill")).to be_false 26 | end 27 | 28 | it "returns true only if all tags match" do 29 | multiple_tag_options = Ec2::Blackout::Options.new(:exclude_by_tag => ["Name=foo.*", "Owner=joe"]) 30 | expect(multiple_tag_options.matches_exclude_tags?("Name" => "foobar", "Owner" => "joe")).to be_true 31 | expect(multiple_tag_options.matches_exclude_tags?("Name" => "foobar", "Owner" => "bill")).to be_false 32 | end 33 | 34 | it "returns false if there are no exclude tags specified" do 35 | empty_options = Ec2::Blackout::Options.new 36 | expect(empty_options.matches_exclude_tags?("Name" => "foobar")).to be_false 37 | end 38 | 39 | it "returns false if no tags match" do 40 | regex_options = Ec2::Blackout::Options.new(:exclude_by_tag => ["Name=foobar"]) 41 | expect(regex_options.matches_exclude_tags?({"Name" => "blerk"})).to be_false 42 | end 43 | 44 | it "handles equals signs in the value" do 45 | equals_options = Ec2::Blackout::Options.new(:exclude_by_tag => ["Name=foo=bar"]) 46 | expect(equals_options.matches_exclude_tags?("Name" => "foo=bar")).to be_true 47 | end 48 | 49 | end 50 | 51 | 52 | describe "#matches_include_tags" do 53 | 54 | it "matches by regular expression" do 55 | regex_options = Ec2::Blackout::Options.new(:include_by_tag => ["Name=foo.*"]) 56 | expect(regex_options.matches_include_tags?({"Name" => "foobar"})).to be_true 57 | end 58 | 59 | it "matches by tag name only if no value is given in the options" do 60 | tag_name_only_options = Ec2::Blackout::Options.new(:include_by_tag => ["Name"]) 61 | expect(tag_name_only_options.matches_include_tags?("Name" => "foobar")).to be_true 62 | expect(tag_name_only_options.matches_include_tags?("Owner" => "bill")).to be_false 63 | end 64 | 65 | it "returns true only if all tags match" do 66 | multiple_tag_options = Ec2::Blackout::Options.new(:include_by_tag => ["Name=foo.*", "Owner=joe"]) 67 | expect(multiple_tag_options.matches_include_tags?("Name" => "foobar", "Owner" => "joe")).to be_true 68 | expect(multiple_tag_options.matches_include_tags?("Name" => "foobar", "Owner" => "bill")).to be_false 69 | end 70 | 71 | it "returns true if there are no include tags specified" do 72 | empty_options = Ec2::Blackout::Options.new 73 | expect(empty_options.matches_include_tags?("Name" => "foobar")).to be_true 74 | end 75 | 76 | it "returns false if no tags match" do 77 | regex_options = Ec2::Blackout::Options.new(:include_by_tag => ["Name=foo.*"]) 78 | expect(regex_options.matches_include_tags?({"Name" => "blerk"})).to be_false 79 | end 80 | 81 | it "handles equals signs in the value" do 82 | equals_options = Ec2::Blackout::Options.new(:include_by_tag => ["Name=foo=bar"]) 83 | expect(equals_options.matches_include_tags?("Name" => "foo=bar")).to be_true 84 | end 85 | 86 | end 87 | 88 | 89 | describe "#regions" do 90 | 91 | it "provides default regions if none are specified" do 92 | options = Ec2::Blackout::Options.new 93 | expect(options.regions).to eq Ec2::Blackout::Options::DEFAULT_REGIONS 94 | end 95 | 96 | end 97 | 98 | 99 | end 100 | -------------------------------------------------------------------------------- /spec/ec2-blackout/shutdown_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ec2::Blackout::Shutdown do 4 | 5 | describe "#execute" do 6 | 7 | it "should shut down only stoppable resources" do 8 | stoppable_group = stoppable_resource(Ec2::Blackout::AutoScalingGroup) 9 | stoppable_group.should_receive(:stop) 10 | 11 | unstoppable_group = unstoppable_resource(Ec2::Blackout::AutoScalingGroup) 12 | unstoppable_group.should_not_receive(:stop) 13 | 14 | stoppable_instance = stoppable_resource(Ec2::Blackout::Ec2Instance) 15 | stoppable_instance.should_receive(:stop) 16 | 17 | unstoppable_instance = unstoppable_resource(Ec2::Blackout::Ec2Instance) 18 | unstoppable_instance.should_not_receive(:stop) 19 | 20 | groups = [stoppable_group, unstoppable_group] 21 | instances = [stoppable_instance, unstoppable_instance] 22 | 23 | Ec2::Blackout::AutoScalingGroup.stub(:groups).and_return(groups) 24 | Ec2::Blackout::Ec2Instance.stub(:running_instances).and_return(instances) 25 | 26 | shutdown = Ec2::Blackout::Shutdown.new(double, Ec2::Blackout::Options.new(:regions => ["ap-southeast-2"])) 27 | shutdown.execute 28 | end 29 | 30 | it "should shut down instances in all regions given by the options" do 31 | options = Ec2::Blackout::Options.new(:regions => ["ap-southeast-1", "ap-southeast-2"]) 32 | 33 | Ec2::Blackout::AutoScalingGroup.should_receive(:groups).with("ap-southeast-1", anything).and_return([]) 34 | Ec2::Blackout::AutoScalingGroup.should_receive(:groups).with("ap-southeast-2", anything).and_return([]) 35 | Ec2::Blackout::Ec2Instance.should_receive(:running_instances).with("ap-southeast-1", anything).and_return([]) 36 | Ec2::Blackout::Ec2Instance.should_receive(:running_instances).with("ap-southeast-2", anything).and_return([]) 37 | 38 | shutdown = Ec2::Blackout::Shutdown.new(double, options) 39 | shutdown.execute 40 | end 41 | 42 | it "should not stop instances if the dry run option has been specified" do 43 | options = Ec2::Blackout::Options.new(:dry_run => true, :regions => ["ap-southeast-2"]) 44 | stoppable_group = stoppable_resource(Ec2::Blackout::AutoScalingGroup) 45 | stoppable_group.should_not_receive(:stop) 46 | 47 | stoppable_instance = stoppable_resource(Ec2::Blackout::Ec2Instance) 48 | stoppable_instance.should_not_receive(:stop) 49 | 50 | Ec2::Blackout::AutoScalingGroup.stub(:groups).and_return([stoppable_group]) 51 | Ec2::Blackout::Ec2Instance.stub(:running_instances).and_return([stoppable_instance]) 52 | 53 | shutdown = Ec2::Blackout::Shutdown.new(double, options) 54 | shutdown.execute 55 | end 56 | 57 | end 58 | 59 | 60 | def stoppable_resource(type) 61 | resource(type, true) 62 | end 63 | 64 | def unstoppable_resource(type) 65 | resource(type, false) 66 | end 67 | 68 | def resource(type, stoppable) 69 | resource = double(type) 70 | resource.should_receive(:stoppable?).and_return(stoppable) 71 | resource 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /spec/ec2-blackout/startup_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ec2::Blackout::Startup do 4 | 5 | describe "#execute" do 6 | 7 | it "should start up only startable resources" do 8 | startable_group = startable_resource(Ec2::Blackout::AutoScalingGroup) 9 | startable_group.should_receive(:start) 10 | 11 | unstartable_group = unstartable_resource(Ec2::Blackout::AutoScalingGroup) 12 | unstartable_group.should_not_receive(:start) 13 | 14 | startable_instance = startable_resource(Ec2::Blackout::Ec2Instance) 15 | startable_instance.should_receive(:start) 16 | 17 | unstartable_instance = unstartable_resource(Ec2::Blackout::Ec2Instance) 18 | unstartable_instance.should_not_receive(:start) 19 | 20 | groups = [startable_group, unstartable_group] 21 | instances = [startable_instance, unstartable_instance] 22 | 23 | Ec2::Blackout::AutoScalingGroup.stub(:groups).and_return(groups) 24 | Ec2::Blackout::Ec2Instance.stub(:stopped_instances).and_return(instances) 25 | 26 | startup = Ec2::Blackout::Startup.new(double, Ec2::Blackout::Options.new(:regions => ["ap-southeast-2"])) 27 | startup.execute 28 | end 29 | 30 | it "should start up instances in all regions given by the options" do 31 | options = Ec2::Blackout::Options.new(:regions => ["ap-southeast-1", "ap-southeast-2"]) 32 | 33 | Ec2::Blackout::AutoScalingGroup.should_receive(:groups).with("ap-southeast-1", anything).and_return([]) 34 | Ec2::Blackout::AutoScalingGroup.should_receive(:groups).with("ap-southeast-2", anything).and_return([]) 35 | Ec2::Blackout::Ec2Instance.should_receive(:stopped_instances).with("ap-southeast-1", anything).and_return([]) 36 | Ec2::Blackout::Ec2Instance.should_receive(:stopped_instances).with("ap-southeast-2", anything).and_return([]) 37 | 38 | startup = Ec2::Blackout::Startup.new(double, options) 39 | startup.execute 40 | end 41 | 42 | it "should not start instances if the dry run option has been specified" do 43 | options = Ec2::Blackout::Options.new(:dry_run => true, :regions => ["ap-southeast-2"]) 44 | startable_group = startable_resource(Ec2::Blackout::AutoScalingGroup) 45 | startable_group.should_not_receive(:start) 46 | 47 | startable_instance = startable_resource(Ec2::Blackout::Ec2Instance) 48 | startable_instance.should_not_receive(:start) 49 | 50 | Ec2::Blackout::AutoScalingGroup.stub(:groups).and_return([startable_group]) 51 | Ec2::Blackout::Ec2Instance.stub(:stopped_instances).and_return([startable_instance]) 52 | 53 | startup = Ec2::Blackout::Startup.new(double, options) 54 | startup.execute 55 | end 56 | 57 | end 58 | 59 | 60 | def startable_resource(type) 61 | resource(type, true) 62 | end 63 | 64 | def unstartable_resource(type) 65 | resource(type, false) 66 | end 67 | 68 | def resource(type, startable) 69 | resource = double(type) 70 | resource.should_receive(:startable?).and_return(startable) 71 | resource 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'ec2-blackout' 2 | 3 | # Make sure we don't accidentally hit a real AWS service 4 | AWS.stub! 5 | --------------------------------------------------------------------------------