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 +2 -0
- data/README.md +51 -6
- data/ec2-blackout.gemspec +2 -1
- data/lib/ec2-blackout.rb +4 -1
- data/lib/ec2-blackout/auto_scaling_group.rb +94 -0
- data/lib/ec2-blackout/commands.rb +6 -15
- data/lib/ec2-blackout/ec2_instance.rb +107 -0
- data/lib/ec2-blackout/options.rb +55 -0
- data/lib/ec2-blackout/shutdown.rb +19 -38
- data/lib/ec2-blackout/startup.rb +18 -43
- data/lib/ec2-blackout/version.rb +1 -1
- data/spec/ec2-blackout/auto_scaling_group_spec.rb +196 -0
- data/spec/ec2-blackout/ec2_instance_spec.rb +185 -0
- data/spec/ec2-blackout/options_spec.rb +99 -0
- data/spec/ec2-blackout/shutdown_spec.rb +74 -0
- data/spec/ec2-blackout/startup_spec.rb +74 -0
- data/spec/spec_helper.rb +4 -0
- metadata +35 -3
data/Gemfile
CHANGED
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
|
-
|
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
|
-
|
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
|
data/ec2-blackout.gemspec
CHANGED
@@ -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)/})
|
data/lib/ec2-blackout.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
require "ec2-blackout/version"
|
2
2
|
require 'commander'
|
3
3
|
require 'aws-sdk'
|
4
|
-
require 'ec2-blackout/
|
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
|
24
|
-
c.option '-x', '--exclude-by-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.
|
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
|
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.
|
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,
|
5
|
-
@ui, @
|
5
|
+
def initialize(ui, options)
|
6
|
+
@ui, @options = ui, options
|
6
7
|
end
|
7
8
|
|
8
9
|
def execute
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
data/lib/ec2-blackout/startup.rb
CHANGED
@@ -1,59 +1,34 @@
|
|
1
|
+
require 'colorize'
|
1
2
|
|
2
3
|
class Ec2::Blackout::Startup
|
3
4
|
|
4
|
-
def initialize(ui,
|
5
|
-
@ui, @
|
5
|
+
def initialize(ui, options)
|
6
|
+
@ui, @options = ui, options
|
6
7
|
end
|
7
8
|
|
8
9
|
def execute
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
data/lib/ec2-blackout/version.rb
CHANGED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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.
|
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:
|
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
|