ec2-blackout 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in ec2-sleeper.gemspec
4
4
  gemspec
5
+
6
+ gem "rspec"
data/README.md CHANGED
@@ -6,13 +6,15 @@ Do you stop EC2 instances out of business hours?
6
6
 
7
7
  If you don't, `ec2-blackout` could save you money
8
8
 
9
- `ec2-blackout` is a command-line tool to stop running EC2 instances.
9
+ `ec2-blackout` is a command-line tool to stop running EC2 instances and Auto Scaling Groups.
10
10
 
11
11
  Use ec2-blackout to shutdown EC2 instances when they are idle, for example when you are not in the office.
12
12
 
13
13
  If an instance has an Elastic IP address, `ec2-blackout` will reassociate the EIP when the instance is started.
14
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
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
+
16
18
  Certinaly not suitable for production instances but development and test instances can generally be shutdown overnight to save money.
17
19
 
18
20
  ## Installation
@@ -23,13 +25,24 @@ It is recommended you create an access policy using Amazon IAM
23
25
 
24
26
  1. Sign in to your AWS management console and go to the IAM section
25
27
  2. Create a group and paste in the following policy
26
-
28
+ ```json
27
29
  {
28
30
  "Statement": [
31
+ {
32
+ "Action": [
33
+ "autoscaling:CreateOrUpdateTags",
34
+ "autoscaling:DeleteTags",
35
+ "autoscaling:DescribeAutoScalingGroups",
36
+ "autoscaling:SetDesiredCapacity"
37
+ ],
38
+ "Effect": "Allow",
39
+ "Resource": "*"
40
+ },
29
41
  {
30
42
  "Action": [
31
43
  "ec2:StartInstances",
32
44
  "ec2:StopInstances",
45
+ "ec2:DescribeTags",
33
46
  "ec2:CreateTags",
34
47
  "ec2:DeleteTags",
35
48
  "ec2:DescribeInstances",
@@ -38,10 +51,11 @@ It is recommended you create an access policy using Amazon IAM
38
51
  "ec2:AssociateAddress"
39
52
  ],
40
53
  "Effect": "Allow",
41
- "Resource": "\*"
54
+ "Resource": "*"
42
55
  }
43
56
  ]
44
57
  }
58
+ ```
45
59
 
46
60
  3. Create a user account and download the access key.
47
61
  4. Add the user to the previously created group.
@@ -52,8 +66,16 @@ Once installed you need to export your AWS credentials
52
66
  export AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
53
67
  export AWS_SECRET_ACCESS_KEY=YOUR_SECREY_KEY
54
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
+
55
75
  ## Usage
56
76
 
77
+ To get help on the commands available:
78
+
57
79
  $ ec2-blackout --help
58
80
 
59
81
  To run a blackout across all AWS regions:
@@ -64,15 +86,38 @@ To run a blackout across a subset of AWS regions:
64
86
 
65
87
  $ ec2-blackout on --regions us-east-1,us-west-1
66
88
 
67
- To run a blackout but exclude instances that have been tagged:
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:
68
112
 
69
- $ ec2-blackout on --exclude-by-tag do_not_blackout
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'
70
115
 
71
116
  To leave a blackout and start the instances that were previously stopped:
72
117
 
73
118
  $ ec2-blackout off
74
119
 
75
- `ec2-blackout` also provides a dry-run using the `--dry-run` option.
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.
76
121
 
77
122
 
78
123
  ## Contributing
@@ -2,13 +2,14 @@
2
2
  require File.expand_path('../lib/ec2-blackout/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ["Stephen Bartlett"]
5
+ gem.authors = ["Stephen Bartlett", "Charles Blaxland"]
6
6
  gem.email = ["stephenb@rtlett.org"]
7
7
  gem.description = Ec2::Blackout.description
8
8
  gem.summary = Ec2::Blackout.summary
9
9
  gem.homepage = "https://github.com/srbartlett/ec2-blackout"
10
10
  gem.add_dependency 'commander'
11
11
  gem.add_dependency 'aws-sdk'
12
+ gem.add_dependency 'colorize'
12
13
  gem.files = `git ls-files`.split($\)
13
14
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
15
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
@@ -1,6 +1,9 @@
1
1
  require "ec2-blackout/version"
2
2
  require 'commander'
3
3
  require 'aws-sdk'
4
- require 'ec2-blackout/shutdown'
4
+ require 'ec2-blackout/options'
5
+ require 'ec2-blackout/auto_scaling_group'
6
+ require 'ec2-blackout/ec2_instance'
5
7
  require 'ec2-blackout/startup'
8
+ require 'ec2-blackout/shutdown'
6
9
 
@@ -0,0 +1,94 @@
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
@@ -7,26 +7,18 @@ program :description, Ec2::Blackout.summary
7
7
 
8
8
  default_command :help
9
9
 
10
- def aws
11
- AWS::EC2.new(:access_key_id => ENV['AWS_ACCESS_KEY_ID'],
12
- :secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'])
13
- end
14
-
15
- DEFAULT_REGIONS = ['us-east-1', 'us-west-1', 'us-west-2', 'eu-west-1', 'ap-southeast-1', 'ap-southeast-2',
16
- 'ap-northeast-1', 'sa-east-1'].join(',')
17
-
18
10
  command "on" do |c|
19
11
  c.syntax = '[options]'
20
12
  c.summary = 'Shutdown EC2 instances'
21
13
  c.description = 'For each AWS Region, find running EC2 instances and shut them down.'
22
14
 
23
- c.option '-r', '--regions STRING', 'Comma separated list of regions to search. Defaults to all regions'
24
- c.option '-x', '--exclude-by-tag STRING', 'Exclude instances with the specified tag.'
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'
25
18
  c.option '-d', '--dry-run', 'Find instances without stopping them'
26
19
 
27
20
  c.action do |args, options|
28
- options.default :regions => DEFAULT_REGIONS
29
- Ec2::Blackout::Shutdown.new(self, aws, options.__hash__).execute
21
+ Ec2::Blackout::Shutdown.new(self, Ec2::Blackout::Options.new(options.__hash__)).execute
30
22
  end
31
23
  end
32
24
 
@@ -35,12 +27,11 @@ command "off" do |c|
35
27
  c.summary = 'Start up EC2 instances'
36
28
  c.description = 'For each AWS Region, find EC2 instances previously shutdown by ec2-blackout and start them up.'
37
29
 
38
- c.option '-r', '--regions STRING', 'Comma separated list of regions to search. Defaults to all regions'
30
+ c.option '-r', '--regions region1,region2', Array, 'Comma separated list of regions to search. Defaults to all regions'
39
31
  c.option '-f', '--force', 'Force start up regardless of who shut them dowm'
40
32
  c.option '-d', '--dry-run', 'Find instances without starting them'
41
33
 
42
34
  c.action do |args, options|
43
- options.default :regions => DEFAULT_REGIONS
44
- Ec2::Blackout::Startup.new(self, aws, options.__hash__).execute
35
+ Ec2::Blackout::Startup.new(self, Ec2::Blackout::Options.new(options.__hash__)).execute
45
36
  end
46
37
  end
@@ -0,0 +1,107 @@
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
+ else
45
+ true
46
+ end
47
+ end
48
+
49
+ def startable?
50
+ if @instance.status != :stopped
51
+ [false, "instance is not in stopped state"]
52
+ elsif @options.force
53
+ true
54
+ elsif !tags[TIMESTAMP_TAG_NAME]
55
+ [false, "instance was not originally stopped by ec2-blackout"]
56
+ else
57
+ true
58
+ end
59
+ end
60
+
61
+ def to_s
62
+ s = "instance #{@instance.id}"
63
+ s += " (#{tags['Name']})" if tags['Name']
64
+ s
65
+ end
66
+
67
+
68
+ private
69
+
70
+ def self.instances(region, options, instance_status)
71
+ AWS::EC2.new(:region => region).instances.filter('instance-state-name', instance_status).map do |instance|
72
+ Ec2::Blackout::Ec2Instance.new(instance, options)
73
+ end
74
+ end
75
+
76
+
77
+ def tag
78
+ @instance.add_tag(TIMESTAMP_TAG_NAME, :value => Time.now.utc)
79
+ if @instance.has_elastic_ip?
80
+ @instance.add_tag(EIP_TAG_NAME, :value => @instance.elastic_ip.allocation_id)
81
+ end
82
+ end
83
+
84
+ def untag
85
+ @instance.tags.delete(TIMESTAMP_TAG_NAME)
86
+ @instance.tags.delete(EIP_TAG_NAME)
87
+ end
88
+
89
+ def tags
90
+ @tags ||= @instance.tags.to_h
91
+ end
92
+
93
+ def associate_eip
94
+ eip = @instance.tags[EIP_TAG_NAME]
95
+ if eip
96
+ attempts = 0
97
+ begin
98
+ @instance.associate_elastic_ip(eip)
99
+ rescue
100
+ sleep eip_retry_delay_seconds
101
+ attempts += 1
102
+ retry if attempts * eip_retry_delay_seconds < 5*60 # 5 mins
103
+ end
104
+ end
105
+ end
106
+
107
+ end
@@ -0,0 +1,55 @@
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
@@ -1,53 +1,34 @@
1
+ require 'colorize'
1
2
 
2
3
  class Ec2::Blackout::Shutdown
3
4
 
4
- def initialize(ui, aws, options)
5
- @ui, @aws, @options = ui, aws, options
5
+ def initialize(ui, options)
6
+ @ui, @options = ui, options
6
7
  end
7
8
 
8
9
  def execute
9
- regions.each do |region|
10
- @ui.say "Checking region: #{region}"
11
- AWS.memoize do
12
- @aws.regions[region].instances.each do |instance|
13
- if stoppable? instance
14
- @ui.say "-> Stopping instance: #{instance.id}, name: #{instance.tags['Name']}"
15
- unless dry_run?
16
- tag(instance)
17
- instance.stop
18
- end
19
- elsif instance.status == :running
20
- @ui.say "-> Skipping instance: #{instance.id}, name: #{instance.tags['Name']}, region: #{region}"
21
- end
22
- end
23
- end
10
+ @ui.say 'Dry run specified - no instances will be stopped'.bold
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))
24
16
  end
25
17
  @ui.say 'Done!'
26
18
  end
27
19
 
28
20
  private
29
21
 
30
- def regions
31
- @options[:regions] ? @options[:regions].split(',') : Ec2::Blackout.regions
32
- end
33
-
34
- def stoppable? instance
35
- (instance.status == :running && exclude_tag && !instance.tags.to_h.key?(exclude_tag)) or
36
- (instance.status == :running && exclude_tag.nil?)
37
- end
38
-
39
- def exclude_tag
40
- @options[:exclude_by_tag]
41
- end
42
-
43
- def dry_run?
44
- @options[:dry_run]
45
- end
46
-
47
- def tag instance
48
- instance.add_tag('ec2:blackout:on', :value => Time.now.utc)
49
- if instance.has_elastic_ip?
50
- instance.add_tag('ec2:blackout:eip', :value => instance.elastic_ip)
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
51
31
  end
52
32
  end
33
+
53
34
  end
@@ -1,59 +1,34 @@
1
+ require 'colorize'
1
2
 
2
3
  class Ec2::Blackout::Startup
3
4
 
4
- def initialize(ui, aws, options)
5
- @ui, @aws, @options = ui, aws, options
5
+ def initialize(ui, options)
6
+ @ui, @options = ui, options
6
7
  end
7
8
 
8
9
  def execute
9
- regions.each do |region|
10
- @ui.say "Checking region: #{region}"
11
- AWS.memoize do
12
- @aws.regions[region].instances.each do |instance|
13
- if startable? instance
14
- @ui.say "-> Starting instance: #{instance.id}, name: #{instance.tags['Name']}."
15
- unless dry_run?
16
- instance.tags.delete('ec2:blackout:on')
17
- instance.start
18
- associate_eip(instance)
19
- end
20
- elsif instance.status == :stopped
21
- @ui.say "-> Skipping instance: #{instance.id}, name: #{instance.tags['Name'] || 'N/A'}"
22
- end
23
- end
24
- end
10
+ @ui.say 'Dry run specified - no instances will be started'.bold
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))
25
16
  end
26
17
  @ui.say 'Done!'
27
18
  end
28
19
 
29
20
  private
30
21
 
31
- def regions
32
- @options[:regions] ? @options[:regions].split(',') : Ec2::Blackout.regions
33
- end
34
-
35
- def startable? instance
36
- (instance.status == :stopped && instance.tags.to_h.key?('ec2:blackout:on')) or
37
- (instance.status == :stopped && @options[:force])
38
- end
39
-
40
- def dry_run?
41
- @options[:dry_run]
42
- end
43
-
44
- def associate_eip instance
45
- eip = instance.tags['ec2:blackout:eip']
46
- if eip
47
- @ui.say("Associating Elastic IP #{eip} to #{instance.id}")
48
- attempts = 0
49
- begin
50
- instance.associate_elastic_ip(eip)
51
- rescue
52
- sleep 5
53
- attempts += 1
54
- retry if attempts < 60 # 5 mins
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}"
55
30
  end
56
- instance.tags.delete('ec2:blackout:eip')
57
31
  end
58
32
  end
33
+
59
34
  end
@@ -1,6 +1,6 @@
1
1
  module Ec2
2
2
  module Blackout
3
- VERSION = "0.0.6"
3
+ VERSION = "0.0.7"
4
4
 
5
5
  def self.name
6
6
  'ec2-blackout'
@@ -0,0 +1,196 @@
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
@@ -0,0 +1,185 @@
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
+ it "saves the associated elastic IP as a tag" do
40
+ aws_instance.stub(:has_elastic_ip?).and_return(true)
41
+ eip = double(:allocation_id => "eipalloc-eab23c0d")
42
+ aws_instance.stub(:elastic_ip).and_return(eip)
43
+ aws_instance.should_receive(:add_tag).with(Ec2Instance::EIP_TAG_NAME, :value => "eipalloc-eab23c0d")
44
+ instance.stop
45
+ end
46
+
47
+ end
48
+
49
+ describe "#start" do
50
+ let!(:aws_instance) { double("ec2 instance") }
51
+ let!(:instance) { stubbed_startable_instance(aws_instance) }
52
+
53
+ it "starts the instance" do
54
+ aws_instance.should_receive(:start)
55
+ instance.start
56
+ end
57
+
58
+ it "remove ec2 blackout tags from the instance" do
59
+ tags = ec2_tags(Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-12 02:35:52 UTC', Ec2Instance::EIP_TAG_NAME => 'eipalloc-eab23c0d')
60
+ aws_instance.stub(:tags).and_return(tags)
61
+ tags.should_receive(:delete).with(Ec2Instance::TIMESTAMP_TAG_NAME)
62
+ tags.should_receive(:delete).with(Ec2Instance::EIP_TAG_NAME)
63
+ instance.start
64
+ end
65
+
66
+ it "reattaches the previous elastic IP" do
67
+ tags = ec2_tags(Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-12 02:35:52 UTC', Ec2Instance::EIP_TAG_NAME => 'eipalloc-eab23c0d')
68
+ aws_instance.stub(:tags).and_return(tags)
69
+ aws_instance.should_receive(:associate_elastic_ip).with("eipalloc-eab23c0d")
70
+ instance.start
71
+ end
72
+
73
+ it "should retry attaching the elastic IP if it fails" do
74
+ tags = ec2_tags(Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-12 02:35:52 UTC', Ec2Instance::EIP_TAG_NAME => 'eipalloc-eab23c0d')
75
+ aws_instance.stub(:tags).and_return(tags)
76
+ aws_instance.should_receive(:associate_elastic_ip).and_raise(:error)
77
+ aws_instance.should_receive(:associate_elastic_ip)
78
+ instance.eip_retry_delay_seconds = 0
79
+ instance.start
80
+ end
81
+ end
82
+
83
+ describe "#stoppable?" do
84
+ let!(:aws_instance) { double("ec2 instance") }
85
+ let!(:instance) { stubbed_stoppable_instance(aws_instance) }
86
+
87
+ it "returns true if the instance meets all conditions for being stoppable" do
88
+ stoppable, reason = instance.stoppable?
89
+ expect(stoppable).to be_true
90
+ end
91
+
92
+ it "returns false if the instance is not running" do
93
+ aws_instance.stub(:status).and_return(:stopped)
94
+ stoppable, reason = instance.stoppable?
95
+ expect(stoppable).to be_false
96
+ end
97
+
98
+ it "returns false if the instance belongs to an autoscaling group" do
99
+ aws_instance.stub(:tags).and_return(ec2_tags('aws:autoscaling:groupName' => 'foobar'))
100
+ stoppable, reason = instance.stoppable?
101
+ expect(stoppable).to be_false
102
+ end
103
+
104
+ it "returns false if the instance matches the exclude tags specified in the options" do
105
+ options = Ec2::Blackout::Options.new(:exclude_by_tag => ["foo=bar"])
106
+ instance = stubbed_stoppable_instance(aws_instance, options)
107
+ aws_instance.stub(:tags).and_return(ec2_tags("foo" => "bar"))
108
+ stoppable, reason = instance.stoppable?
109
+ expect(stoppable).to be_false
110
+ end
111
+
112
+ it "returns false if the instance does not match the include tags specified in the options" do
113
+ options = Ec2::Blackout::Options.new(:include_by_tag => ["foo=bar"])
114
+ instance = stubbed_stoppable_instance(aws_instance, options)
115
+ aws_instance.stub(:tags).and_return(ec2_tags("foo" => "baz"))
116
+ stoppable, reason = instance.stoppable?
117
+ expect(stoppable).to be_false
118
+ end
119
+ end
120
+
121
+ describe "#startable?" do
122
+ let!(:aws_instance) { double("ec2 instance") }
123
+ let!(:instance) { stubbed_startable_instance(aws_instance) }
124
+
125
+ it "returns true if the instance meets all conditions for being startable" do
126
+ startable, reason = instance.startable?
127
+ expect(startable).to be_true
128
+ end
129
+
130
+ it "returns false if the instance does not have the ec2 blackout timestamp tag" do
131
+ aws_instance.stub(:tags).and_return(ec2_tags({}))
132
+ startable, reason = instance.startable?
133
+ expect(startable).to be_false
134
+ end
135
+
136
+ it "returns true if the force tag was specified even if the instance does not have the ec2 blackout timestamp tag" do
137
+ options = Ec2::Blackout::Options.new(:force => true)
138
+ instance = stubbed_startable_instance(aws_instance, options)
139
+ aws_instance.stub(:tags).and_return(ec2_tags({}))
140
+ startable, reason = instance.startable?
141
+ expect(startable).to be_true
142
+ end
143
+ end
144
+
145
+
146
+ def stubbed_stoppable_instance(underlying_aws_stub, options = Options.new)
147
+ underlying_aws_stub.stub(:status).and_return(:running)
148
+ underlying_aws_stub.stub(:has_elastic_ip?).and_return(false)
149
+ underlying_aws_stub.stub(:tags).and_return(ec2_tags({}))
150
+ underlying_aws_stub.stub(:add_tag)
151
+ underlying_aws_stub.stub(:stop)
152
+ Ec2Instance.new(underlying_aws_stub, options)
153
+ end
154
+
155
+ def stubbed_startable_instance(underlying_aws_stub, options = Options.new)
156
+ underlying_aws_stub.stub(:status).and_return(:stopped)
157
+ underlying_aws_stub.stub(:has_elastic_ip?).and_return(false)
158
+ underlying_aws_stub.stub(:tags).and_return(ec2_tags(Ec2Instance::TIMESTAMP_TAG_NAME => '2014-02-13 02:35:52 UTC'))
159
+ underlying_aws_stub.stub(:associate_elastic_ip)
160
+ underlying_aws_stub.stub(:start)
161
+ Ec2Instance.new(underlying_aws_stub, options)
162
+ end
163
+
164
+ def expect_instance_filter(state, region)
165
+ instances_stub = double("instances")
166
+ ec2_stub = double("ec2 instance", :instances => instances_stub)
167
+ AWS::EC2.should_receive(:new).with(:region => region).and_return(ec2_stub)
168
+ instances_stub.should_receive(:filter).with('instance-state-name', state).and_return([double, double])
169
+ end
170
+
171
+ def ec2_tags(tags_hash)
172
+ # Hack to add a "to_h" method to hash instance (ruby 1.9 doesn't have it, but 2.0 does).
173
+ # This lets us use a simple hash instead of an instance of AWS::EC2::ResourceTagCollection to
174
+ # represent the instance tags.
175
+ if !tags_hash.respond_to?(:to_h)
176
+ def tags_hash.to_h
177
+ self
178
+ end
179
+ end
180
+ tags_hash
181
+ end
182
+
183
+ end
184
+
185
+ end
@@ -0,0 +1,99 @@
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
@@ -0,0 +1,74 @@
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
@@ -0,0 +1,74 @@
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
@@ -0,0 +1,4 @@
1
+ require 'ec2-blackout'
2
+
3
+ # Make sure we don't accidentally hit a real AWS service
4
+ AWS.stub!
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ec2-blackout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Stephen Bartlett
9
+ - Charles Blaxland
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2013-08-14 00:00:00.000000000 Z
13
+ date: 2014-02-17 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: commander
@@ -43,6 +44,22 @@ dependencies:
43
44
  - - ! '>='
44
45
  - !ruby/object:Gem::Version
45
46
  version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: colorize
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
46
63
  description: ! "\n ec2-blackout is a command line tool to shutdown EC2 instances.\n\n
47
64
  \ It's main purpose is to save you money by stopping EC2 instances during\n
48
65
  \ out of office hours.\n "
@@ -61,10 +78,19 @@ files:
61
78
  - bin/ec2-blackout
62
79
  - ec2-blackout.gemspec
63
80
  - lib/ec2-blackout.rb
81
+ - lib/ec2-blackout/auto_scaling_group.rb
64
82
  - lib/ec2-blackout/commands.rb
83
+ - lib/ec2-blackout/ec2_instance.rb
84
+ - lib/ec2-blackout/options.rb
65
85
  - lib/ec2-blackout/shutdown.rb
66
86
  - lib/ec2-blackout/startup.rb
67
87
  - lib/ec2-blackout/version.rb
88
+ - spec/ec2-blackout/auto_scaling_group_spec.rb
89
+ - spec/ec2-blackout/ec2_instance_spec.rb
90
+ - spec/ec2-blackout/options_spec.rb
91
+ - spec/ec2-blackout/shutdown_spec.rb
92
+ - spec/ec2-blackout/startup_spec.rb
93
+ - spec/spec_helper.rb
68
94
  homepage: https://github.com/srbartlett/ec2-blackout
69
95
  licenses: []
70
96
  post_install_message:
@@ -89,4 +115,10 @@ rubygems_version: 1.8.23
89
115
  signing_key:
90
116
  specification_version: 3
91
117
  summary: A command-line tool to shutdown EC2 instances.
92
- test_files: []
118
+ test_files:
119
+ - spec/ec2-blackout/auto_scaling_group_spec.rb
120
+ - spec/ec2-blackout/ec2_instance_spec.rb
121
+ - spec/ec2-blackout/options_spec.rb
122
+ - spec/ec2-blackout/shutdown_spec.rb
123
+ - spec/ec2-blackout/startup_spec.rb
124
+ - spec/spec_helper.rb