furnish-aws 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +20 -0
- data/Gemfile +4 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +59 -0
- data/Rakefile +32 -0
- data/furnish-aws.gemspec +30 -0
- data/lib/furnish/aws/version.rb +6 -0
- data/lib/furnish/provisioners/aws.rb +101 -0
- data/lib/furnish/provisioners/ec2.rb +372 -0
- data/lib/furnish/provisioners/security_group.rb +380 -0
- data/test/helper.rb +61 -0
- data/test/test_aws_base_class.rb +30 -0
- data/test/test_ec2.rb +225 -0
- data/test/test_security_group.rb +322 -0
- metadata +188 -0
@@ -0,0 +1,380 @@
|
|
1
|
+
require 'furnish/provisioners/aws'
|
2
|
+
|
3
|
+
module Furnish # :nodoc:
|
4
|
+
module Provisioner # :nodoc:
|
5
|
+
#
|
6
|
+
# Provision an EC2 Security Group. Currently receives no input and yields
|
7
|
+
# the group id created as output.
|
8
|
+
#
|
9
|
+
# See the attributes and constructor for more information about creating
|
10
|
+
# this provisioner.
|
11
|
+
#
|
12
|
+
class SecurityGroup < AWS
|
13
|
+
|
14
|
+
##
|
15
|
+
# :attr: region
|
16
|
+
#
|
17
|
+
# Provided by Furnish::Provisioner::AWS#region -- required for this provisioner.
|
18
|
+
#
|
19
|
+
|
20
|
+
##
|
21
|
+
# :attr: group_name
|
22
|
+
#
|
23
|
+
# Name of the group. Supply a string to name it explicitly, or :auto (the
|
24
|
+
# default). If :auto, will pick a name for you based on #group_prefix
|
25
|
+
# and a random string of numbers that do not already correspond to a
|
26
|
+
# group.
|
27
|
+
#
|
28
|
+
furnish_property :group_name,
|
29
|
+
"Name of the group. Supply a string to name it. Default is :auto -- will pick a name for you and overwrite this accessor."
|
30
|
+
|
31
|
+
##
|
32
|
+
# :attr: group_prefix
|
33
|
+
#
|
34
|
+
# If #group_name is :auto, will use this value as a prefix. See
|
35
|
+
# #group_name for more information.
|
36
|
+
#
|
37
|
+
furnish_property :group_prefix,
|
38
|
+
"If :group_name is :auto, will use this value as prefix, and a string of random numbers as the postfix. Default is 'furnish-auto-'",
|
39
|
+
String
|
40
|
+
|
41
|
+
##
|
42
|
+
# :attr: description
|
43
|
+
#
|
44
|
+
# String description of the group. Optional.
|
45
|
+
#
|
46
|
+
furnish_property :description,
|
47
|
+
"Description of the group. Optional",
|
48
|
+
String
|
49
|
+
|
50
|
+
##
|
51
|
+
# :attr: vpc
|
52
|
+
#
|
53
|
+
# VPC identifier. Optional, has significant effect on how security groups
|
54
|
+
# work. See the EC2 and VPC documentation for details.
|
55
|
+
#
|
56
|
+
furnish_property :vpc,
|
57
|
+
"VPC identifier. Optional, see EC2 and VPC documentation for details.",
|
58
|
+
String
|
59
|
+
|
60
|
+
##
|
61
|
+
# :attr: kill_instances
|
62
|
+
#
|
63
|
+
# If true (the default), at shutdown time will terminate any instances
|
64
|
+
# that belong to the group provisioned by this provisioner so the
|
65
|
+
# security group can be destroyed.
|
66
|
+
#
|
67
|
+
furnish_property :kill_instances,
|
68
|
+
"If true (default), on shutdown will force termination of all instances in the security group."
|
69
|
+
|
70
|
+
##
|
71
|
+
# :attr: ingress
|
72
|
+
#
|
73
|
+
# Hash containing two keys: :authorize and :revoke, which each contain
|
74
|
+
# array of arrays. Passed to authorize_ingress and revoke_ingress
|
75
|
+
# respectively.
|
76
|
+
#
|
77
|
+
# Example:
|
78
|
+
#
|
79
|
+
# Furnish::Provisioners::SecurityGroup.new(
|
80
|
+
# :ingress => {
|
81
|
+
# :authorize => [
|
82
|
+
# [:tcp, 22]
|
83
|
+
# [:tcp, (1024..1234)]
|
84
|
+
# [:tcp, 53, "127.0.0.1/32"]
|
85
|
+
# ]
|
86
|
+
# }
|
87
|
+
# )
|
88
|
+
#
|
89
|
+
furnish_property :ingress,
|
90
|
+
"Hash containing two keys: :authorize and :revoke, which each contain array of arrays. Passed to authorize_ingress and revoke_ingress respectively.",
|
91
|
+
Hash
|
92
|
+
|
93
|
+
##
|
94
|
+
# :attr: egress
|
95
|
+
#
|
96
|
+
# Same as #ingress but for egress rules. Note that egress rules only work
|
97
|
+
# against groups that belong to a VPCs.
|
98
|
+
#
|
99
|
+
furnish_property :egress,
|
100
|
+
"Same as :ingress, but only work on VPCs.",
|
101
|
+
Hash
|
102
|
+
|
103
|
+
##
|
104
|
+
# :attr: allow_ping
|
105
|
+
#
|
106
|
+
# If true, allow ping from all hosts. If an array of strings, allow ping
|
107
|
+
# from that set of CIDR networks.
|
108
|
+
#
|
109
|
+
furnish_property :allow_ping,
|
110
|
+
"If true, allow ping from all hosts, if array of strings, treats them as CIDR networks."
|
111
|
+
|
112
|
+
##
|
113
|
+
# :attr: disallow_ping
|
114
|
+
#
|
115
|
+
# Inverse of #allow_ping.
|
116
|
+
#
|
117
|
+
furnish_property :disallow_ping,
|
118
|
+
"Inverse of :allow_ping."
|
119
|
+
|
120
|
+
##
|
121
|
+
# the group_id as returned from EC2, only set after provisioning has
|
122
|
+
# occurred.
|
123
|
+
attr_reader :group_id
|
124
|
+
|
125
|
+
#
|
126
|
+
# Construct a new security group provisioner.
|
127
|
+
#
|
128
|
+
# Example:
|
129
|
+
#
|
130
|
+
# obj = Furnish::Provisioners::SecurityGroup.new(
|
131
|
+
# :access_key => "foo",
|
132
|
+
# :secret_key => "bar",
|
133
|
+
# :region => "my-region"
|
134
|
+
# )
|
135
|
+
#
|
136
|
+
# See Furnish::Provisioner::AWS for how constructor arguments and
|
137
|
+
# attributes interact.
|
138
|
+
#
|
139
|
+
def initialize(args)
|
140
|
+
super
|
141
|
+
check_region
|
142
|
+
|
143
|
+
@ingress ||= { :authorize => [], :revoke => [] }
|
144
|
+
@egress ||= { :authorize => [], :revoke => [] }
|
145
|
+
@allow_ping ||= []
|
146
|
+
@disallow_ping ||= []
|
147
|
+
@group_name ||= :auto
|
148
|
+
@group_prefix ||= "furnish-auto-"
|
149
|
+
@kill_instances = args.has_key?(:kill_instances) ? args[:kill_instances] : true
|
150
|
+
@group_id = nil
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# Generate a group name (only used if :auto is assigned to #group_name).
|
155
|
+
# Ensures generated names are not already in use. Returns the value it
|
156
|
+
# ends up with.
|
157
|
+
#
|
158
|
+
def generate_group_name
|
159
|
+
unless group_prefix
|
160
|
+
raise ArgumentError, "group_prefix must be set when auto-generating security group names"
|
161
|
+
end
|
162
|
+
|
163
|
+
tmp_name = nil
|
164
|
+
|
165
|
+
loop do
|
166
|
+
tmp_name = group_prefix + (0..rand(10).to_i).map { rand(0..9).to_s }.join("")
|
167
|
+
|
168
|
+
if_debug(3) do
|
169
|
+
puts "Seeing if security group name #{tmp_name} is taken"
|
170
|
+
end
|
171
|
+
|
172
|
+
break unless find_group_by_name(tmp_name)
|
173
|
+
|
174
|
+
if_debug(3) do
|
175
|
+
puts "group exists, waiting to try again"
|
176
|
+
end
|
177
|
+
|
178
|
+
sleep 0.3
|
179
|
+
end
|
180
|
+
|
181
|
+
return tmp_name
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
185
|
+
# Applies network rules (see #ingress, #egress, #allow_ping,
|
186
|
+
# #disallow_ping attributes) to a created security group.
|
187
|
+
#
|
188
|
+
# Raises ArgumentError if security groups do not belong to a VPC and
|
189
|
+
# egress filters are set.
|
190
|
+
#
|
191
|
+
def apply_group_rules(group)
|
192
|
+
# XXX this meta programs passing the contents set on the object to the
|
193
|
+
# group object.
|
194
|
+
[:allow_ping, :disallow_ping].each do |sym|
|
195
|
+
if send(sym).kind_of?(Array)
|
196
|
+
ary = send(sym)
|
197
|
+
|
198
|
+
if_debug(3) do
|
199
|
+
puts "group #{group.id}/#{group.name}: applying #{sym} to #{ary.inspect}"
|
200
|
+
end
|
201
|
+
|
202
|
+
group.send(sym, *ary)
|
203
|
+
elsif send(sym)
|
204
|
+
if_debug(3) do
|
205
|
+
puts "group #{group.id}/#{group.name}: applying #{sym} to all instances"
|
206
|
+
end
|
207
|
+
|
208
|
+
group.send(sym) # XXX "everybody"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# XXX this mess is similar to above, but generates the call_name as well.
|
213
|
+
# e.g.: "authorize_egress" comes from @egress[:authorize], and each
|
214
|
+
# inner array in the data structure is expanded as args in the call.
|
215
|
+
[:authorize, :revoke].each do |part|
|
216
|
+
[:ingress, :egress].each do |type|
|
217
|
+
if send(type) and ary = send(type)[part] and ary.kind_of?(Array) and !ary.empty?
|
218
|
+
if type == :egress and !group.vpc?
|
219
|
+
raise ArgumentError, "egress filtering was configured for #{group.id}/#{group.name} and it is not a VPC security group"
|
220
|
+
end
|
221
|
+
|
222
|
+
ary.each do |rule|
|
223
|
+
if_debug(3) do
|
224
|
+
puts "group #{group.id}/#{group.name}: applying #{part}_#{type} with values #{rule.inspect}"
|
225
|
+
end
|
226
|
+
|
227
|
+
group.send("#{part}_#{type}", *rule.flatten)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
#
|
235
|
+
# Queries the API for all instances, returns any that are not in the
|
236
|
+
# terminated state.
|
237
|
+
#
|
238
|
+
def find_secgroup_running_instances(group)
|
239
|
+
group.instances.select { |i| i.status != :terminated }
|
240
|
+
end
|
241
|
+
|
242
|
+
#
|
243
|
+
# Searches for a group by its name, returns the AWS::EC2::SecurityGroup
|
244
|
+
# instance.
|
245
|
+
#
|
246
|
+
def find_group_by_name(group_name)
|
247
|
+
return nil unless group_name
|
248
|
+
ec2.security_groups.filter('group-name', [group_name.to_s]).first
|
249
|
+
end
|
250
|
+
|
251
|
+
#
|
252
|
+
# Only used during shutdown; returns the group if it exists, otherwise
|
253
|
+
# logs and returns false. Intended to isolate idempotent function.
|
254
|
+
#
|
255
|
+
def load_group_for_shutdown
|
256
|
+
unless group_id
|
257
|
+
prov_name = furnish_group_name
|
258
|
+
this_group_name = group_name
|
259
|
+
|
260
|
+
if_debug(2) do
|
261
|
+
puts "group id for #{this_group_name} did not exist during provisioner shutdown for #{prov_name || "unknown"} -- did this get provisioned?"
|
262
|
+
end
|
263
|
+
|
264
|
+
return false
|
265
|
+
end
|
266
|
+
|
267
|
+
group = ec2.security_groups[group_id]
|
268
|
+
|
269
|
+
unless group and group.exists?
|
270
|
+
this_group_name = group_name
|
271
|
+
|
272
|
+
if_debug(2) do
|
273
|
+
puts "group #{this_group_name} did not exist during shutdown; not attempting to deprovision it and returning success"
|
274
|
+
end
|
275
|
+
|
276
|
+
return false
|
277
|
+
end
|
278
|
+
|
279
|
+
return group
|
280
|
+
end
|
281
|
+
|
282
|
+
#
|
283
|
+
# only used during shutdown; if kill_instances is true, terminates all
|
284
|
+
# the non-terminated machines in the group so it can be deleted. Will
|
285
|
+
# keep trying this until all machines are in a terminated state.
|
286
|
+
#
|
287
|
+
def terminate_instances_for_shutdown(group)
|
288
|
+
running_instances = find_secgroup_running_instances(group)
|
289
|
+
|
290
|
+
if running_instances.count > 0
|
291
|
+
if kill_instances
|
292
|
+
if_debug(3) do
|
293
|
+
puts "group #{group.id}/#{group.name} is flagged to kill instances in its group on deprovision"
|
294
|
+
end
|
295
|
+
|
296
|
+
until (instances = find_secgroup_running_instances(group)).empty?
|
297
|
+
if_debug(1) do
|
298
|
+
puts "Trying to destroy security group #{group.name}, but instances are still bound to it."
|
299
|
+
puts instances.map(&:id).inspect
|
300
|
+
puts "Terminating instances, sleeping, and trying again."
|
301
|
+
end
|
302
|
+
|
303
|
+
instances.each do |i|
|
304
|
+
i.terminate rescue nil
|
305
|
+
end
|
306
|
+
|
307
|
+
sleep 10
|
308
|
+
end
|
309
|
+
else
|
310
|
+
raise "instances #{running_instances.map(&:id).inspect} are still running for group #{group.id}/#{group.name} and kill_instances is off. Cannot continue."
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
#
|
316
|
+
# Provision a security group. Group name generation happens here if
|
317
|
+
# #group_name is set to :auto, if the group already exists, it will be
|
318
|
+
# used and then rules will be applied to it. Returns (and sets on the
|
319
|
+
# object) the group_id as returned by EC2.
|
320
|
+
#
|
321
|
+
def startup(args={})
|
322
|
+
# FIXME VPC as argument?
|
323
|
+
|
324
|
+
@group_name = generate_group_name if group_name == :auto
|
325
|
+
|
326
|
+
begin
|
327
|
+
group =
|
328
|
+
ec2.security_groups.create(
|
329
|
+
group_name,
|
330
|
+
:description => description,
|
331
|
+
:vpc => vpc
|
332
|
+
)
|
333
|
+
rescue ::AWS::EC2::Errors::InvalidGroup::Duplicate => e
|
334
|
+
group = find_group_by_name(group_name)
|
335
|
+
raise e unless group
|
336
|
+
|
337
|
+
my_name = furnish_group_name # yay instance_eval
|
338
|
+
|
339
|
+
if_debug(1) do
|
340
|
+
puts "#{group.id}/#{group.name} already existed during #{my_name || "ungrouped"} provision -- applying rules and continuing"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
apply_group_rules(group)
|
345
|
+
@group_id = group.id
|
346
|
+
|
347
|
+
return({ :security_group_ids => (args[:security_group_ids] || []) + [group_id] })
|
348
|
+
end
|
349
|
+
|
350
|
+
#
|
351
|
+
# Deprovision a security group. If the group does not exist, immediately
|
352
|
+
# returns a true result.
|
353
|
+
#
|
354
|
+
# If #kill_instances is true, it will terminate (and wait for the
|
355
|
+
# termination to succeed) any machines that are using the security group
|
356
|
+
# that are not yet terminated.
|
357
|
+
#
|
358
|
+
# After deleting the group via the API, it will return true if the group
|
359
|
+
# no longer exists.
|
360
|
+
#
|
361
|
+
def shutdown(args={})
|
362
|
+
return { } unless group = load_group_for_shutdown
|
363
|
+
|
364
|
+
terminate_instances_for_shutdown(group)
|
365
|
+
group.delete
|
366
|
+
|
367
|
+
return group.exists? ? false : { }
|
368
|
+
end
|
369
|
+
|
370
|
+
#
|
371
|
+
# Report method as required by Furnish: yields the name of the group,
|
372
|
+
# it's group id, and any vpc information if VPC is in use.
|
373
|
+
#
|
374
|
+
def report
|
375
|
+
vpc_str = vpc ? " vpc_id: #{vpc}" : ""
|
376
|
+
["name: #{group_name}, id: #{group_id}#{vpc_str}"]
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
|
3
|
+
if ENV["COVERAGE"]
|
4
|
+
require 'simplecov'
|
5
|
+
SimpleCov.start do
|
6
|
+
add_filter '/test/'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
unless File.exist?(".acknowledged")
|
11
|
+
$stderr.puts <<-'EOF'
|
12
|
+
/.--------------.\
|
13
|
+
// \\
|
14
|
+
// \\
|
15
|
+
|| .-..----. .-. .--. ||
|
16
|
+
||( ( '-..-'|.-.||.-.|||
|
17
|
+
|| \ \ || || ||||_||||
|
18
|
+
||._) ) || \'-'/||-' ||
|
19
|
+
\\'-' `' `-' `' //
|
20
|
+
\\ //
|
21
|
+
\\______________//
|
22
|
+
'--------------'
|
23
|
+
|
24
|
+
These tests have no mocks -- they provision real AWS services (machines,
|
25
|
+
security groups, etc) and tear them down.
|
26
|
+
|
27
|
+
It's your job to set your AWS environment variables to an environment that's
|
28
|
+
safe, and bound to a credit card you're comfortable spending money on.
|
29
|
+
Likewise, if the tests fail, there's a high likelihood you'll need to clean
|
30
|
+
machines up manually.
|
31
|
+
|
32
|
+
If you're ok with this, `touch .acknowledged` in the root of this repository
|
33
|
+
and run the tests again.
|
34
|
+
|
35
|
+
EOF
|
36
|
+
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
|
40
|
+
eval File.read('.aws-env') if File.exist?('.aws-env')
|
41
|
+
|
42
|
+
unless ENV["AWS_ACCESS_KEY_ID"] and ENV["AWS_SECRET_ACCESS_KEY"]
|
43
|
+
if ENV["AWS_ACCESS_KEY"] and ENV["AWS_SECRET_KEY"]
|
44
|
+
# XXX the above vars are the "new school" and so if we have them and not
|
45
|
+
# the "old school", copy them.
|
46
|
+
ENV["AWS_ACCESS_KEY_ID"] = ENV["AWS_ACCESS_KEY"]
|
47
|
+
ENV["AWS_SECRET_ACCESS_KEY"] = ENV["AWS_SECRET_KEY"]
|
48
|
+
else
|
49
|
+
$stderr.puts <<-EOF
|
50
|
+
These tests cannot run without AWS environment variables set.
|
51
|
+
|
52
|
+
You can create a file in the root of the repository called .aws-env that
|
53
|
+
will be loaded as a ruby file to set these values.
|
54
|
+
EOF
|
55
|
+
|
56
|
+
exit 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
require 'furnish/test'
|
61
|
+
require 'minitest/autorun'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'furnish/provisioners/aws'
|
3
|
+
|
4
|
+
class TestAWSProvisioner < Furnish::RunningSchedulerTestCase
|
5
|
+
def setup
|
6
|
+
@klass = Furnish::Provisioner::AWS
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_constructor
|
11
|
+
assert_raises(ArgumentError, "arguments must be a hash") { @klass.new }
|
12
|
+
assert_raises(ArgumentError, "AWS credentials must be provided to the provisioner.") { @klass.new({}) }
|
13
|
+
|
14
|
+
# XXX should not raise
|
15
|
+
obj = @klass.new(:access_key => "qwe123", :secret_key => "password1")
|
16
|
+
|
17
|
+
assert_equal("qwe123", obj.access_key, "access key is available from object after set in constructor")
|
18
|
+
assert_equal("password1", obj.secret_key, "secret key is available from object after set in constructor")
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_ec2
|
22
|
+
obj = @klass.new(:access_key => "qwe123", :secret_key => "password1", :region => "us-east-1")
|
23
|
+
assert_kind_of(AWS::EC2::Region, obj.ec2)
|
24
|
+
refute_nil(obj.region, "region is non-nil")
|
25
|
+
assert_equal(obj.region, obj.ec2.name, "region propogates to ec2 object")
|
26
|
+
|
27
|
+
obj.region = "us-west-1"
|
28
|
+
assert_equal(obj.region, obj.ec2.name, "region propogates to ec2 object after being changed")
|
29
|
+
end
|
30
|
+
end
|