furnish-aws 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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