internuity-awsum 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Internuity Ltd, Andrew Timberlake <andrew@andrewtimberlake.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,42 @@
1
+ =Awsum
2
+
3
+ Awsum (Pronounced Awesome) is a library for working with Amazon web services.
4
+ The concept of Awsum is to expose the AWS library is the most ruby way possible
5
+ allowing you to work with objects in a very natural way.
6
+
7
+ ==Quick Start
8
+
9
+ #Create a snapshot for every volume of every instance you own
10
+ ec2 = Awsum::Ec2.new(<access key>, <secret key>)
11
+ ec2.instances.each do |instance|
12
+ instance.volumes.each do |volume|
13
+ volume.create_snapshot
14
+ end
15
+ end
16
+
17
+ ==Working with different Regions
18
+ You can use blocks to wrap your calls for a specific Region
19
+ ec2.region('eu-west-1').use do
20
+ #Run an instance in the eu-west-1 region
21
+ run_instance(...)
22
+ end
23
+
24
+ ==Using the library on an EC2 instance
25
+
26
+ There are two methods specifically for using a library on an EC2 instance
27
+ Awsum::Ec2#me
28
+ Awsum::Ec2#user_data
29
+
30
+ To extend the quick start example, you could do
31
+
32
+ #Create a snapshot of every volume of the currently running instance
33
+ ec2 = Awsum::Ec2.new(<access key>, <secret key>)
34
+ ec2.me.volumes.each do |volume|
35
+ volume.create_snapshot
36
+ end
37
+
38
+ ==Note:
39
+
40
+ Awsum is currently under active development and only supports EC2 at the moment.
41
+
42
+ Once EC2 is complete, I will focus on S3, SQS, CloudSpace and then others
data/Rakefile ADDED
@@ -0,0 +1,85 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
7
+ require 'awsum'
8
+
9
+ desc 'Default: run unit tests.'
10
+ task :default => [:clean, :test]
11
+
12
+ desc 'Run tests'
13
+ Rake::TestTask.new(:test) do |t|
14
+ t.libs << 'lib'
15
+ t.pattern = 'test/**/test_*.rb'
16
+ t.verbose = true
17
+ end
18
+
19
+ desc 'Run code coverage'
20
+ task :coverage do |t|
21
+ puts `rcov -T #{Dir.glob('test/**/test_*.rb').join(' ')}`
22
+ end
23
+
24
+ desc 'Start an IRB session with all necessary files required.'
25
+ task :shell do |t|
26
+ chdir File.dirname(__FILE__)
27
+ exec 'irb -I lib/ -I lib/awsum -r rubygems -r awsum'
28
+ end
29
+
30
+ desc 'Generate documentation.'
31
+ Rake::RDocTask.new(:rdoc) do |rdoc|
32
+ rdoc.rdoc_dir = 'doc'
33
+ rdoc.title = 'AWSum'
34
+ rdoc.options << '--line-numbers' << '--inline-source'
35
+ rdoc.rdoc_files.include('README*')
36
+ rdoc.rdoc_files.include('lib/**/*.rb')
37
+ end
38
+
39
+ desc 'Clean up files.'
40
+ task :clean do |t|
41
+ FileUtils.rm_rf "doc"
42
+ FileUtils.rm_rf "tmp"
43
+ FileUtils.rm_rf "pkg"
44
+ end
45
+
46
+ spec = Gem::Specification.new do |s|
47
+ s.name = "awsum"
48
+ s.version = Awsum::VERSION
49
+ s.author = "Andrew Timberlake"
50
+ s.email = "andrew@andrewtimberlake.com"
51
+ s.homepage = "http://www.internuity.net/projects/awsum"
52
+ s.platform = Gem::Platform::RUBY
53
+ s.summary = "Ruby library for working with Amazon Web Services"
54
+ s.files = FileList["README*",
55
+ "LICENSE",
56
+ "Rakefile",
57
+ "{lib,test}/**/*"].to_a
58
+ s.require_path = "lib"
59
+ s.test_files = FileList["test/**/test_*.rb"].to_a + FileList["test/fixtures/**/*.xml"].to_a
60
+ s.rubyforge_project = "awsum"
61
+ s.has_rdoc = true
62
+ s.extra_rdoc_files = FileList["README*"].to_a
63
+ s.rdoc_options << '--line-numbers' << '--inline-source'
64
+ s.add_development_dependency 'thoughtbot-shoulda'
65
+ s.add_development_dependency 'mocha'
66
+ end
67
+
68
+ desc "Release new version"
69
+ task :release => [:test, :sync_docs, :gem] do
70
+ require 'rubygems'
71
+ require 'rubyforge'
72
+ r = RubyForge.new
73
+ r.login
74
+ r.add_release spec.rubyforge_project,
75
+ spec.name,
76
+ spec.version,
77
+ File.join("pkg", "#{spec.name}-#{spec.version}.gem")
78
+ end
79
+
80
+ desc "Generate a gemspec file for GitHub"
81
+ task :gemspec do
82
+ File.open("#{spec.name}.gemspec", 'w') do |f|
83
+ f.write spec.to_ruby
84
+ end
85
+ end
data/lib/awsum.rb ADDED
@@ -0,0 +1,24 @@
1
+ # AWSum is a library facilitating access to Amazon's web services in (hopefully)
2
+ # a very object-oriented, ruby way.
3
+ #
4
+ # Author:: Andrew Timberlake
5
+ # Copyright:: Copyright (c) 2009 Internuity Ltd
6
+ # Licence:: MIT License (http://www.opensource.org/licenses/mit-license.php)
7
+ #
8
+
9
+ require 'parser'
10
+ require 'requestable'
11
+ require 'support'
12
+
13
+ require 'ec2/ec2'
14
+ require 's3/s3'
15
+
16
+ module Awsum
17
+
18
+ VERSION = "0.1"
19
+
20
+ API_VERSION = '2008-12-01'
21
+ SIGNATURE_VERSION = 2
22
+ end
23
+
24
+
data/lib/ec2/ec2.rb ADDED
@@ -0,0 +1,656 @@
1
+ require 'ec2/address'
2
+ require 'ec2/availability_zone'
3
+ require 'ec2/image'
4
+ require 'ec2/instance'
5
+ require 'ec2/keypair'
6
+ require 'ec2/security_group'
7
+ require 'ec2/snapshot'
8
+ require 'ec2/region'
9
+ require 'ec2/volume'
10
+
11
+ module Awsum
12
+ # Handles all interaction with Amazon EC2
13
+ #
14
+ # ==Getting Started
15
+ # Create an Awsum::Ec2 object and begin calling methods on it.
16
+ # require 'rubygems'
17
+ # require 'awsum'
18
+ # ec2 = Awsum::Ec2.new('your access id', 'your secret key')
19
+ # images = ec2.my_images
20
+ # ...
21
+ #
22
+ # All calls to EC2 can be done directly in this class, or through a more object oriented way through the various returned classes
23
+ #
24
+ # ==Examples
25
+ # ec2.image('ami-ABCDEF').run
26
+ #
27
+ # ec2.instance('i-123456789').volumes.each do |vol|
28
+ # vol.create_snapsot
29
+ # end
30
+ #
31
+ # ec2.regions.each do |region|
32
+ # region.use
33
+ # images.each do |img|
34
+ # puts "#{img.id} - #{region.name}"
35
+ # end
36
+ # end
37
+ # end
38
+ #
39
+ # ==Errors
40
+ # All methods will raise an Awsum::Error if an error is returned from Amazon
41
+ #
42
+ # ==Missing Methods
43
+ # * ConfirmProductInstance
44
+ # * ModifyImageAttribute
45
+ # * DescribeImageAttribute
46
+ # * ResetImageAttribute
47
+ # If you need any of this functionality, please consider getting involved and help complete this library.
48
+ class Ec2
49
+ include Awsum::Requestable
50
+
51
+ # Create an new ec2 instance
52
+ #
53
+ # The access_key and secret_key are both required to do any meaningful work.
54
+ #
55
+ # If you want to get these keys from environment variables, you can do that in your code as follows:
56
+ # ec2 = Awsum::Ec2.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
57
+ def initialize(access_key = nil, secret_key = nil)
58
+ @access_key = access_key
59
+ @secret_key = secret_key
60
+ end
61
+
62
+ # Retrieve a list of available Images
63
+ #
64
+ # ===Options:
65
+ # * <tt>:image_ids</tt> - array of Image id's, default: []
66
+ # * <tt>:owners</tt> - array of owner id's, default: []
67
+ # * <tt>:executable_by</tt> - array of user id's who have executable permission, default: []
68
+ def images(options = {})
69
+ options = {:image_ids => [], :owners => [], :executable_by => []}.merge(options)
70
+ action = 'DescribeImages'
71
+ params = {
72
+ 'Action' => action
73
+ }
74
+ #Add options
75
+ params.merge!(array_to_params(options[:image_ids], "ImageId"))
76
+ params.merge!(array_to_params(options[:owners], "Owner"))
77
+ params.merge!(array_to_params(options[:executable_by], "ExecutableBy"))
78
+
79
+ response = send_query_request(params)
80
+ parser = Awsum::Ec2::ImageParser.new(self)
81
+ parser.parse(response.body)
82
+ end
83
+
84
+ # Retrieve all Image(s) owned by you
85
+ def my_images
86
+ images :owners => 'self'
87
+ end
88
+
89
+ # Retrieve a single Image
90
+ def image(image_id)
91
+ images(:image_ids => [image_id])[0]
92
+ end
93
+
94
+ # Register an Image
95
+ def register_image(image_location)
96
+ action = 'RegisterImage'
97
+ params = {
98
+ 'Action' => action,
99
+ 'ImageLocation' => image_location
100
+ }
101
+
102
+ response = send_query_request(params)
103
+ parser = Awsum::Ec2::RegisterImageParser.new(self)
104
+ parser.parse(response.body)
105
+ end
106
+
107
+ # Deregister an Image. Once deregistered, you can no longer launch the Image
108
+ def deregister_image(image_id)
109
+ action = 'DeregisterImage'
110
+ params = {
111
+ 'Action' => action,
112
+ 'ImageId' => image_id
113
+ }
114
+
115
+ response = send_query_request(params)
116
+ response.is_a?(Net::HTTPSuccess)
117
+ end
118
+
119
+ # Launch an ec2 Instance
120
+ #
121
+ # ===Options:
122
+ # * <tt>:min</tt> - The minimum number of instances to launch. Default: 1
123
+ # * <tt>:max</tt> - The maximum number of instances to launch. Default: 1
124
+ # * <tt>:key_name</tt> - The name of the key pair with which to launch instances
125
+ # * <tt>:security_groups</tt> - The names of security groups to associate launched instances with
126
+ # * <tt>:user_data</tt> - User data made available to instances (Note: Must be 16K or less, will be base64 encoded by Awsum)
127
+ # * <tt>:instance_type</tt> - The size of the instances to launch, can be one of [m1.small, m1.large, m1.xlarge, c1.medium, c1.xlarge], default is m1.small
128
+ # * <tt>:availability_zone</tt> - The name of the availability zone to launch this Instance in
129
+ # * <tt>:kernel_id</tt> - The ID of the kernel with which to launch instances
130
+ # * <tt>:ramdisk_id</tt> - The ID of the RAM disk with which to launch instances
131
+ # * <tt>:block_device_map</tt> - A 'hash' of mappings. E.g. {'instancestore0' => 'sdb'}
132
+ def run_instances(image_id, options = {})
133
+ options = {:min => 1, :max => 1}.merge(options)
134
+ action = 'RunInstances'
135
+ params = {
136
+ 'Action' => action,
137
+ 'ImageId' => image_id,
138
+ 'MinCount' => options[:min],
139
+ 'MaxCount' => options[:max],
140
+ 'KeyName' => options[:key_name],
141
+ 'UserData' => options[:user_data].nil? ? nil : Base64::encode64(options[:user_data]).gsub(/\n/, ''),
142
+ 'InstanceType' => options[:instance_type],
143
+ 'Placement.AvailabilityZone' => options[:availability_zone],
144
+ 'KernelId' => options[:kernel_id],
145
+ 'RamdiskId' => options[:ramdisk_id]
146
+ }
147
+ if options[:block_device_map].respond_to?(:keys)
148
+ map = options[:block_device_map]
149
+ map.keys.each_with_index do |key, i|
150
+ params["BlockDeviceMapping.#{i+1}.VirtualName"] = key
151
+ params["BlockDeviceMapping.#{i+1}.DeviceName"] = map[key]
152
+ end
153
+ else
154
+ raise ArgumentError.new("options[:block_device_map] - must be a key => value map") unless options[:block_device_map].nil?
155
+ end
156
+ params.merge!(array_to_params(options[:security_groups], "SecurityGroup"))
157
+
158
+ response = send_query_request(params)
159
+ parser = Awsum::Ec2::InstanceParser.new(self)
160
+ parser.parse(response.body)
161
+ end
162
+ alias_method :launch_instances, :run_instances
163
+
164
+ #Retrieve the information on a number of Instance(s)
165
+ def instances(*instance_ids)
166
+ action = 'DescribeInstances'
167
+ params = {
168
+ 'Action' => action
169
+ }
170
+ params.merge!(array_to_params(instance_ids, 'InstanceId'))
171
+
172
+ response = send_query_request(params)
173
+ parser = Awsum::Ec2::InstanceParser.new(self)
174
+ parser.parse(response.body)
175
+ end
176
+
177
+ #Retrieve the information on a single Instance
178
+ def instance(instance_id)
179
+ instances([instance_id])[0]
180
+ end
181
+
182
+ # Retrieves the currently running Instance
183
+ # This should only be run on a running EC2 instance
184
+ def me
185
+ require 'open-uri'
186
+ begin
187
+ instance_id = open('http://169.254.169.254/latest/meta-data/instance-id').read
188
+ instance instance_id
189
+ rescue OpenURI::HTTPError => e
190
+ nil
191
+ end
192
+ end
193
+
194
+ # Retreives the user-data supplied when starting the currently running Instance
195
+ # This should only be run on a running EC2 instance
196
+ def user_data
197
+ require 'open-uri'
198
+ begin
199
+ open('http://169.254.169.254/latest/user-data').read
200
+ rescue OpenURI::HTTPError => e
201
+ nil
202
+ end
203
+ end
204
+
205
+ # Terminates the Instance(s)
206
+ #
207
+ # Returns true if the terminations succeeds, false otherwise
208
+ def terminate_instances(*instance_ids)
209
+ action = 'TerminateInstances'
210
+ params = {
211
+ 'Action' => action
212
+ }
213
+ params.merge!(array_to_params(instance_ids, 'InstanceId'))
214
+
215
+ response = send_query_request(params)
216
+ response.is_a?(Net::HTTPSuccess)
217
+ end
218
+
219
+ #Retrieve the information on a number of Volume(s)
220
+ def volumes(*volume_ids)
221
+ action = 'DescribeVolumes'
222
+ params = {
223
+ 'Action' => action
224
+ }
225
+ params.merge!(array_to_params(volume_ids, 'VolumeId'))
226
+
227
+ response = send_query_request(params)
228
+ parser = Awsum::Ec2::VolumeParser.new(self)
229
+ parser.parse(response.body)
230
+ end
231
+
232
+ # Retreive information on a Volume
233
+ def volume(volume_id)
234
+ volumes(volume_id)[0]
235
+ end
236
+
237
+ # Create a new volume
238
+ #
239
+ # ===Options:
240
+ # * <tt>:size</tt> - The size of the volume to be created (in GB) (<b>NOTE:</b> Required if you are not creating from a snapshot)
241
+ # * <tt>:snapshot_id</tt> - The snapshot id from which to create the volume
242
+ #
243
+ def create_volume(availability_zone, options = {})
244
+ raise ArgumentError.new('You must specify a size if not creating a volume from a snapshot') if options[:snapshot_id].blank? && options[:size].blank?
245
+
246
+ action = 'CreateVolume'
247
+ params = {
248
+ 'Action' => action,
249
+ 'AvailabilityZone' => availability_zone
250
+ }
251
+ params['Size'] = options[:size] unless options[:size].blank?
252
+ params['SnapshotId'] = options[:snapshot_id] unless options[:snapshot_id].blank?
253
+
254
+ response = send_query_request(params)
255
+ parser = Awsum::Ec2::VolumeParser.new(self)
256
+ parser.parse(response.body)[0]
257
+ end
258
+
259
+ # Attach a volume to an instance
260
+ def attach_volume(volume_id, instance_id, device = '/dev/sdh')
261
+ action = 'AttachVolume'
262
+ params = {
263
+ 'Action' => action,
264
+ 'VolumeId' => volume_id,
265
+ 'InstanceId' => instance_id,
266
+ 'Device' => device
267
+ }
268
+
269
+ response = send_query_request(params)
270
+ response.is_a?(Net::HTTPSuccess)
271
+ end
272
+
273
+ # Detach a volume from an instance
274
+ #
275
+ # ===Options
276
+ # * <tt>:instance_id</tt> - The ID of the instance from which the volume will detach
277
+ # * <tt>:device</tt> - The device name
278
+ # * <tt>:force</tt> - Whether to force the detachment. <b>NOTE:</b> If forced you may have data corruption issues.
279
+ def detach_volume(volume_id, options = {})
280
+ action = 'DetachVolume'
281
+ params = {
282
+ 'Action' => action,
283
+ 'VolumeId' => volume_id
284
+ }
285
+ params['InstanceId'] = options[:instance_id] unless options[:instance_id].blank?
286
+ params['Device'] = options[:device] unless options[:device].blank?
287
+ params['Force'] = options[:force] unless options[:force].blank?
288
+
289
+ response = send_query_request(params)
290
+ response.is_a?(Net::HTTPSuccess)
291
+ end
292
+
293
+ # Delete a volume
294
+ def delete_volume(volume_id)
295
+ action = 'DeleteVolume'
296
+ params = {
297
+ 'Action' => action,
298
+ 'VolumeId' => volume_id
299
+ }
300
+
301
+ response = send_query_request(params)
302
+ response.is_a?(Net::HTTPSuccess)
303
+ end
304
+
305
+ # Create a Snapshot of a Volume
306
+ def create_snapshot(volume_id)
307
+ action = 'CreateSnapshot'
308
+ params = {
309
+ 'Action' => action,
310
+ 'VolumeId' => volume_id
311
+ }
312
+
313
+ response = send_query_request(params)
314
+ parser = Awsum::Ec2::SnapshotParser.new(self)
315
+ parser.parse(response.body)[0]
316
+ end
317
+
318
+ # List Snapshot(s)
319
+ def snapshots(*snapshot_ids)
320
+ action = 'DescribeSnapshots'
321
+ params = {
322
+ 'Action' => action
323
+ }
324
+ params.merge!(array_to_params(snapshot_ids, 'SnapshotId'))
325
+
326
+ response = send_query_request(params)
327
+ parser = Awsum::Ec2::SnapshotParser.new(self)
328
+ parser.parse(response.body)
329
+ end
330
+
331
+ # Get the information about a Snapshot
332
+ def snapshot(snapshot_id)
333
+ snapshots(snapshot_id)[0]
334
+ end
335
+
336
+ # Delete a Snapshot
337
+ def delete_snapshot(snapshot_id)
338
+ action = 'DeleteSnapshot'
339
+ params = {
340
+ 'Action' => action,
341
+ 'SnapshotId' => snapshot_id
342
+ }
343
+
344
+ response = send_query_request(params)
345
+ response.is_a?(Net::HTTPSuccess)
346
+ end
347
+
348
+ # List all AvailabilityZone(s)
349
+ def availability_zones(*zone_names)
350
+ action = 'DescribeAvailabilityZones'
351
+ params = {
352
+ 'Action' => action
353
+ }
354
+ params.merge!(array_to_params(zone_names, 'ZoneName'))
355
+
356
+ response = send_query_request(params)
357
+ parser = Awsum::Ec2::AvailabilityZoneParser.new(self)
358
+ parser.parse(response.body)
359
+ end
360
+
361
+ # List all Region(s)
362
+ def regions(*region_names)
363
+ action = 'DescribeRegions'
364
+ params = {
365
+ 'Action' => action
366
+ }
367
+ params.merge!(array_to_params(region_names, 'Region'))
368
+
369
+ response = send_query_request(params)
370
+ parser = Awsum::Ec2::RegionParser.new(self)
371
+ parser.parse(response.body)
372
+ end
373
+
374
+ # List a Region
375
+ def region(region_name)
376
+ regions(region_name)[0]
377
+ end
378
+
379
+ # List Addresses
380
+ def addresses(*public_ips)
381
+ action = 'DescribeAddresses'
382
+ params = {
383
+ 'Action' => action
384
+ }
385
+ params.merge!(array_to_params(public_ips, 'PublicIp'))
386
+
387
+ response = send_query_request(params)
388
+ parser = Awsum::Ec2::AddressParser.new(self)
389
+ parser.parse(response.body)
390
+ end
391
+
392
+ # Get the Address with a specific public ip
393
+ def address(public_ip)
394
+ addresses(public_ip)[0]
395
+ end
396
+
397
+ # Allocate Address
398
+ #
399
+ # Will aquire an elastic ip address for use with your account
400
+ def allocate_address
401
+ action = 'AllocateAddress'
402
+ params = {
403
+ 'Action' => action
404
+ }
405
+
406
+ response = send_query_request(params)
407
+ parser = Awsum::Ec2::AddressParser.new(self)
408
+ parser.parse(response.body)[0]
409
+ end
410
+
411
+ # Associate Address
412
+ #
413
+ # Will link an allocated elastic ip address to an Instance
414
+ #
415
+ # <b>NOTE:</b> If the ip address is already associated with another instance, it will be associated with the new instance.
416
+ #
417
+ # You can run this command more than once and it will not return an error.
418
+ def associate_address(instance_id, public_ip)
419
+ action = 'AssociateAddress'
420
+ params = {
421
+ 'Action' => action,
422
+ 'InstanceId' => instance_id,
423
+ 'PublicIp' => public_ip
424
+ }
425
+
426
+ response = send_query_request(params)
427
+ response.is_a?(Net::HTTPSuccess)
428
+ end
429
+
430
+ # Disassociate Address
431
+ #
432
+ # Will disassociate an allocated elastic ip address from the Instance it's allocated to
433
+ #
434
+ # <b>NOTE:</b> You can run this command more than once and it will not return an error.
435
+ def disassociate_address(public_ip)
436
+ action = 'DisassociateAddress'
437
+ params = {
438
+ 'Action' => action,
439
+ 'PublicIp' => public_ip
440
+ }
441
+
442
+ response = send_query_request(params)
443
+ response.is_a?(Net::HTTPSuccess)
444
+ end
445
+
446
+ # Releases an associated Address
447
+ #
448
+ # <b>NOTE:</b> This is not a direct call to the Amazon web service. This is a safe operation that will first check to see if the address is allocated to an instance and fail if it is
449
+ def release_address(public_ip)
450
+ address = address(public_ip)
451
+
452
+ if address.instance_id.nil?
453
+ action = 'ReleaseAddress'
454
+ params = {
455
+ 'Action' => action,
456
+ 'PublicIp' => public_ip
457
+ }
458
+
459
+ response = send_query_request(params)
460
+ response.is_a?(Net::HTTPSuccess)
461
+ else
462
+ raise 'Address is currently allocated' #FIXME: Add a proper Awsum error here
463
+ end
464
+ end
465
+
466
+ # Releases an associated Address
467
+ #
468
+ # <b>NOTE:</b> This will disassociate an address automatically if it is associated with an instance
469
+ def release_address!(public_ip)
470
+ action = 'ReleaseAddress'
471
+ params = {
472
+ 'Action' => action,
473
+ 'PublicIp' => public_ip
474
+ }
475
+
476
+ response = send_query_request(params)
477
+ response.is_a?(Net::HTTPSuccess)
478
+ end
479
+
480
+ # List KeyPair(s)
481
+ def key_pairs(*key_names)
482
+ action = 'DescribeKeyPairs'
483
+ params = {
484
+ 'Action' => action
485
+ }
486
+ params.merge!(array_to_params(key_names, 'KeyName'))
487
+
488
+ response = send_query_request(params)
489
+ parser = Awsum::Ec2::KeyPairParser.new(self)
490
+ parser.parse(response.body)
491
+ end
492
+
493
+ # Get a single KeyPair
494
+ def key_pair(key_name)
495
+ key_pairs(key_name)[0]
496
+ end
497
+
498
+ # Create a new KeyPair
499
+ def create_key_pair(key_name)
500
+ action = 'CreateKeyPair'
501
+ params = {
502
+ 'Action' => action,
503
+ 'KeyName' => key_name
504
+ }
505
+
506
+ response = send_query_request(params)
507
+ parser = Awsum::Ec2::KeyPairParser.new(self)
508
+ parser.parse(response.body)[0]
509
+ end
510
+
511
+ # Delete a KeyPair
512
+ def delete_key_pair(key_name)
513
+ action = 'DeleteKeyPair'
514
+ params = {
515
+ 'Action' => action,
516
+ 'KeyName' => key_name
517
+ }
518
+
519
+ response = send_query_request(params)
520
+ response.is_a?(Net::HTTPSuccess)
521
+ end
522
+
523
+ # List SecurityGroup(s)
524
+ def security_groups(*group_names)
525
+ action = 'DescribeSecurityGroups'
526
+ params = {
527
+ 'Action' => action
528
+ }
529
+ params.merge!(array_to_params(group_names, 'GroupName'))
530
+
531
+ response = send_query_request(params)
532
+ parser = Awsum::Ec2::SecurityGroupParser.new(self)
533
+ parser.parse(response.body)
534
+ end
535
+
536
+ # Get a single SecurityGroup
537
+ def security_group(group_name)
538
+ security_groups(group_name)[0]
539
+ end
540
+
541
+ # Create a new SecurityGroup
542
+ def create_security_group(name, description)
543
+ action = 'CreateSecurityGroup'
544
+ params = {
545
+ 'Action' => action,
546
+ 'GroupName' => name,
547
+ 'GroupDescription' => description
548
+ }
549
+
550
+ response = send_query_request(params)
551
+ response.is_a?(Net::HTTPSuccess)
552
+ end
553
+
554
+ # Delete a SecurityGroup
555
+ def delete_security_group(group_name)
556
+ action = 'DeleteSecurityGroup'
557
+ params = {
558
+ 'Action' => action,
559
+ 'GroupName' => group_name
560
+ }
561
+
562
+ response = send_query_request(params)
563
+ response.is_a?(Net::HTTPSuccess)
564
+ end
565
+
566
+ # Authorize access on a specific security group
567
+ #
568
+ # ===Options:
569
+ # ====User/Group access
570
+ # * <tt>:source_security_group_name</tt> - Name of the security group to authorize access to when operating on a user/group pair
571
+ # * <tt>:source_security_group_owner_id</tt> - Owner of the security group to authorize access to when operating on a user/group pair
572
+ # ====CIDR IP access
573
+ # * <tt>:ip_protocol</tt> - IP protocol to authorize access to when operating on a CIDR IP (tcp, udp or icmp) (default: tcp)
574
+ # * <tt>:from_port</tt> - Bottom of port range to authorize access to when operating on a CIDR IP. This contains the ICMP type if ICMP is being authorized.
575
+ # * <tt>:to_port</tt> - Top of port range to authorize access to when operating on a CIDR IP. This contains the ICMP type if ICMP is being authorized.
576
+ # * <tt>:cidr_ip</tt> - CIDR IP range to authorize access to when operating on a CIDR IP. (default: 0.0.0.0/0)
577
+ def authorize_security_group_ingress(group_name, options = {})
578
+ got_at_least_one_user_group_option = !options[:source_security_group_name].nil? || !options[:source_security_group_owner_id].nil?
579
+ got_user_group_options = !options[:source_security_group_name].nil? && !options[:source_security_group_owner_id].nil?
580
+ got_at_least_one_cidr_option = !options[:ip_protocol].nil? || !options[:from_port].nil? || !options[:to_port].nil? || !options[:cidr_ip].nil?
581
+ #Add in defaults
582
+ options = {:cidr_ip => '0.0.0.0/0'}.merge(options) if got_at_least_one_cidr_option
583
+ options = {:ip_protocol => 'tcp'}.merge(options) if got_at_least_one_cidr_option
584
+ got_cidr_options = !options[:ip_protocol].nil? && !options[:from_port].nil? && !options[:to_port].nil? && !options[:cidr_ip].nil?
585
+ raise ArgumentError.new('Can only authorize user/group or CIDR IP, not both') if got_at_least_one_user_group_option && got_at_least_one_cidr_option
586
+ raise ArgumentError.new('Need all user/group options when authorizing user/group access') if got_at_least_one_user_group_option && !got_user_group_options
587
+ raise ArgumentError.new('Need all CIDR IP options when authorizing CIDR IP access') if got_at_least_one_cidr_option && !got_cidr_options
588
+ raise ArgumentError.new('ip_protocol can only be one of tcp, udp or icmp') if got_at_least_one_cidr_option && !%w(tcp udp icmp).detect{|p| p == options[:ip_protocol] }
589
+
590
+ action = 'AuthorizeSecurityGroupIngress'
591
+ params = {
592
+ 'Action' => action,
593
+ 'GroupName' => group_name
594
+ }
595
+ params['SourceSecurityGroupName'] = options[:source_security_group_name] unless options[:source_security_group_name].nil?
596
+ params['SourceSecurityGroupOwnerId'] = options[:source_security_group_owner_id] unless options[:source_security_group_owner_id].nil?
597
+ params['IpProtocol'] = options[:ip_protocol] unless options[:ip_protocol].nil?
598
+ params['FromPort'] = options[:from_port] unless options[:from_port].nil?
599
+ params['ToPort'] = options[:to_port] unless options[:to_port].nil?
600
+ params['CidrIp'] = options[:cidr_ip] unless options[:cidr_ip].nil?
601
+
602
+ response = send_query_request(params)
603
+ response.is_a?(Net::HTTPSuccess)
604
+ end
605
+
606
+ # Revoke access on a specific SecurityGroup
607
+ #
608
+ # ===Options:
609
+ # ====User/Group access
610
+ # * <tt>:source_security_group_name</tt> - Name of the security group to authorize access to when operating on a user/group pair
611
+ # * <tt>:source_security_group_owner_id</tt> - Owner of the security group to authorize access to when operating on a user/group pair
612
+ # ====CIDR IP access
613
+ # * <tt>:ip_protocol</tt> - IP protocol to authorize access to when operating on a CIDR IP (tcp, udp or icmp) (default: tcp)
614
+ # * <tt>:from_port</tt> - Bottom of port range to authorize access to when operating on a CIDR IP. This contains the ICMP type if ICMP is being authorized.
615
+ # * <tt>:to_port</tt> - Top of port range to authorize access to when operating on a CIDR IP. This contains the ICMP type if ICMP is being authorized.
616
+ # * <tt>:cidr_ip</tt> - CIDR IP range to authorize access to when operating on a CIDR IP. (default: 0.0.0.0/0)
617
+ def revoke_security_group_ingress(group_name, options = {})
618
+ got_at_least_one_user_group_option = !options[:source_security_group_name].nil? || !options[:source_security_group_owner_id].nil?
619
+ got_user_group_options = !options[:source_security_group_name].nil? && !options[:source_security_group_owner_id].nil?
620
+ got_at_least_one_cidr_option = !options[:ip_protocol].nil? || !options[:from_port].nil? || !options[:to_port].nil? || !options[:cidr_ip].nil?
621
+ #Add in defaults
622
+ options = {:cidr_ip => '0.0.0.0/0'}.merge(options) if got_at_least_one_cidr_option
623
+ options = {:ip_protocol => 'tcp'}.merge(options) if got_at_least_one_cidr_option
624
+ got_cidr_options = !options[:ip_protocol].nil? && !options[:from_port].nil? && !options[:to_port].nil? && !options[:cidr_ip].nil?
625
+ raise ArgumentError.new('Can only authorize user/group or CIDR IP, not both') if got_at_least_one_user_group_option && got_at_least_one_cidr_option
626
+ raise ArgumentError.new('Need all user/group options when revoking user/group access') if got_at_least_one_user_group_option && !got_user_group_options
627
+ raise ArgumentError.new('Need all CIDR IP options when revoking CIDR IP access') if got_at_least_one_cidr_option && !got_cidr_options
628
+ raise ArgumentError.new('ip_protocol can only be one of tcp, udp or icmp') if got_at_least_one_cidr_option && !%w(tcp udp icmp).detect{|p| p == options[:ip_protocol] }
629
+
630
+ action = 'RevokeSecurityGroupIngress'
631
+ params = {
632
+ 'Action' => action,
633
+ 'GroupName' => group_name
634
+ }
635
+ params['SourceSecurityGroupName'] = options[:source_security_group_name] unless options[:source_security_group_name].nil?
636
+ params['SourceSecurityGroupOwnerId'] = options[:source_security_group_owner_id] unless options[:source_security_group_owner_id].nil?
637
+ params['IpProtocol'] = options[:ip_protocol] unless options[:ip_protocol].nil?
638
+ params['FromPort'] = options[:from_port] unless options[:from_port].nil?
639
+ params['ToPort'] = options[:to_port] unless options[:to_port].nil?
640
+ params['CidrIp'] = options[:cidr_ip] unless options[:cidr_ip].nil?
641
+
642
+ response = send_query_request(params)
643
+ response.is_a?(Net::HTTPSuccess)
644
+ end
645
+
646
+ #private
647
+ #The host to make all requests against
648
+ def host
649
+ @host ||= 'ec2.amazonaws.com'
650
+ end
651
+
652
+ def host=(host)
653
+ @host = host
654
+ end
655
+ end
656
+ end