internuity-awsum 0.2

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/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