furnish-aws 0.0.1

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.
@@ -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