bguthrie-awsymandias 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -4,6 +4,23 @@
4
4
 
5
5
  Awsymandias is a library that helps you set up and tear down complicated AWS environments. In addition to offering a clean, fluent domain model for working with instances and groups of instances it allows you to persist role-to-instance-id mappings to SimpleDB, allowing you to manage your stack from multiple different machines. Eventually it will allow you to add arbitrary instance metadata and help you track instance running costs over time.
6
6
 
7
+ I met a hacker from an antique land
8
+ Who said: Two tall and heavy mounts of steel
9
+ Lie in a basement. Near them on a stand,
10
+ Recessed, a dark CRT lies, whose peel’d
11
+ Cracked shell of dullest beige, and blinkenlights,
12
+ Tell that its fact’ry well those old specs read
13
+ Which yet survive, inked on the lifeless thing,
14
+ The die that stamp’d them and the power that fed.
15
+ And on the burned-in screen these words appear:
16
+ “My name is Awsymandias, king of kings:
17
+ Look on my racks, ye Mighty, and despair!”
18
+ No bits at all remain. Not far away
19
+ A data center waits, its humming air
20
+ Host to a boundless cloud by th’hour to pay.
21
+
22
+ (Apologies to Shelley’s original sonnet. Die-hards will please note that it scans and attempts to follow the original meter and rhyme scheme wherever possible.)
23
+
7
24
  == Example
8
25
 
9
26
  # Give the stack a name, and describe its members.
@@ -13,7 +30,7 @@ Awsymandias is a library that helps you set up and tear down complicated AWS env
13
30
  end
14
31
 
15
32
  # Check if we're running by pulling stack description from SDB; if not, launch asynchronously.
16
- stack.launch unless stack.running?
33
+ stack.launch unless stack.launched? || stack.running?
17
34
  until stack.running?
18
35
  sleep(5)
19
36
  end
@@ -26,6 +43,10 @@ Awsymandias is a library that helps you set up and tear down complicated AWS env
26
43
 
27
44
  This should allow you to re-launch and deploy that AWS stack from any one of several different workstations.
28
45
 
46
+ == Contributors
47
+
48
+ * Paul Gross (pgross@gmail.com)
49
+
29
50
  == License
30
51
 
31
52
  (The MIT License)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -0,0 +1,63 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{awsymandias}
5
+ s.version = "0.2.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Brian Guthrie"]
9
+ s.date = %q{2009-07-14}
10
+ s.description = %q{A library for helping you set up, track, and tear down complicated deployment configurations in Amazon EC2.}
11
+ s.email = %q{btguthrie@gmail.com}
12
+ s.extra_rdoc_files = [
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "README.rdoc",
18
+ "Rakefile",
19
+ "VERSION",
20
+ "awsymandias.gemspec",
21
+ "lib/awsymandias.rb",
22
+ "spec/awsymandias_spec.rb",
23
+ "vendor/aws-sdb/LICENSE",
24
+ "vendor/aws-sdb/README",
25
+ "vendor/aws-sdb/Rakefile",
26
+ "vendor/aws-sdb/lib/aws_sdb.rb",
27
+ "vendor/aws-sdb/lib/aws_sdb/error.rb",
28
+ "vendor/aws-sdb/lib/aws_sdb/service.rb",
29
+ "vendor/aws-sdb/spec/aws_sdb/service_spec.rb",
30
+ "vendor/aws-sdb/spec/spec_helper.rb"
31
+ ]
32
+ s.has_rdoc = true
33
+ s.homepage = %q{http://github.com/bguthrie/awsymandias}
34
+ s.rdoc_options = ["--charset=UTF-8"]
35
+ s.require_paths = ["lib"]
36
+ s.rubygems_version = %q{1.3.1}
37
+ s.summary = %q{A library for helping you set up, track, and tear down complicated deployment configurations in Amazon EC2.}
38
+ s.test_files = [
39
+ "spec/awsymandias_spec.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 2
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.3.0"])
48
+ s.add_runtime_dependency(%q<activeresource>, [">= 2.3.0"])
49
+ s.add_runtime_dependency(%q<grempe-amazon-ec2>, [">= 0.4.2"])
50
+ s.add_runtime_dependency(%q<money>, [">= 2.1.3"])
51
+ else
52
+ s.add_dependency(%q<activesupport>, [">= 2.3.0"])
53
+ s.add_dependency(%q<activeresource>, [">= 2.3.0"])
54
+ s.add_dependency(%q<grempe-amazon-ec2>, [">= 0.4.2"])
55
+ s.add_dependency(%q<money>, [">= 2.1.3"])
56
+ end
57
+ else
58
+ s.add_dependency(%q<activesupport>, [">= 2.3.0"])
59
+ s.add_dependency(%q<activeresource>, [">= 2.3.0"])
60
+ s.add_dependency(%q<grempe-amazon-ec2>, [">= 0.4.2"])
61
+ s.add_dependency(%q<money>, [">= 2.1.3"])
62
+ end
63
+ end
data/lib/awsymandias.rb CHANGED
@@ -19,6 +19,25 @@ module Awsymandias
19
19
  end
20
20
  end
21
21
 
22
+ module Support
23
+ module Hash
24
+ # Ganked from ActiveResource 2.3.2.
25
+ def reformat_incoming_param_data(params)
26
+ case params.class.to_s
27
+ when "Hash"
28
+ params.inject({}) do |h,(k,v)|
29
+ h[k.to_s.underscore.tr("-", "_")] = reformat_incoming_param_data(v)
30
+ h
31
+ end
32
+ when "Array"
33
+ params.map { |v| reformat_incoming_param_data(v) }
34
+ else
35
+ params
36
+ end
37
+ end
38
+ end
39
+ end
40
+
22
41
  module EC2
23
42
  class << self
24
43
  # Define the values for AMAZON_ACCESS_KEY_ID and AMAZON_SECRET_ACCESS_KEY_ID to allow for automatic
@@ -69,14 +88,22 @@ module Awsymandias
69
88
  # It wraps the simple hash structures returned by the EC2 gem with a domain model.
70
89
  # It inherits from ARes::B in order to provide simple XML <-> domain model mapping.
71
90
  class Instance < ActiveResource::Base
72
- include ActiveSupport::CoreExtensions::Hash::Conversions::ClassMethods
73
- extend ActiveSupport::CoreExtensions::Hash::Conversions::ClassMethods # unrename_keys
74
-
91
+ include Awsymandias::Support::Hash
92
+ extend Awsymandias::Support::Hash # reformat_incoming_param_data
93
+
75
94
  self.site = "mu"
76
95
 
77
96
  def id; instance_id; end
78
97
  def public_dns; dns_name; end
79
98
  def private_dns; private_dns_name; end
99
+
100
+ def public_ip
101
+ dns_to_ip(public_dns)
102
+ end
103
+
104
+ def private_ip
105
+ dns_to_ip(private_dns)
106
+ end
80
107
 
81
108
  def pending?
82
109
  instance_state.name == "pending"
@@ -96,7 +123,7 @@ module Awsymandias
96
123
  end
97
124
 
98
125
  def reload
99
- load(unrename_keys(
126
+ load(reformat_incoming_param_data(
100
127
  EC2.connection.describe_instances(:instance_id => [ self.instance_id ])["reservationSet"]["item"].
101
128
  first["instancesSet"]["item"].
102
129
  first # Good lord.
@@ -147,8 +174,10 @@ module Awsymandias
147
174
  if reservation_set.nil?
148
175
  []
149
176
  else
150
- reservation_set["item"].first["instancesSet"]["item"].map do |item|
151
- instantiate_record(unrename_keys(item))
177
+ reservation_set["item"].sum([]) do |item_set|
178
+ item_set["instancesSet"]["item"].map do |item|
179
+ instantiate_record(reformat_incoming_param_data(item))
180
+ end
152
181
  end
153
182
  end
154
183
  end
@@ -159,7 +188,7 @@ module Awsymandias
159
188
  raise ActiveResource::ResourceNotFound, "not found: #{id}"
160
189
  else
161
190
  reservation_set["item"].first["instancesSet"]["item"].map do |item|
162
- instantiate_record(unrename_keys(item))
191
+ instantiate_record(reformat_incoming_param_data(item))
163
192
  end.first
164
193
  end
165
194
  end
@@ -174,6 +203,13 @@ module Awsymandias
174
203
  find(instance_id)
175
204
  end
176
205
  end
206
+
207
+ private
208
+
209
+ def dns_to_ip(dns)
210
+ match = dns.match(/(ec2|ip)-(\d+)-(\d+)-(\d+)-(\d+)/)
211
+ match.captures[1..-1].join(".")
212
+ end
177
213
  end
178
214
 
179
215
  # Goal:
@@ -241,13 +277,18 @@ module Awsymandias
241
277
  end
242
278
 
243
279
  def launched?
244
- @instances.any? || restore_from_role_to_instance_id_mapping.any?
280
+ @instances.any? || ( @instances = retrieve_role_to_instance_id_mapping ).any?
245
281
  end
246
282
 
247
283
  def running?
248
284
  launched? && @instances.values.all?(&:running?)
249
285
  end
250
286
 
287
+ def running_cost
288
+ return Money.new(0) unless launched?
289
+ @instances.values.sum { |instance| instance.running_cost }
290
+ end
291
+
251
292
  def inspect
252
293
  ( [ "Environment #{@name}, running? #{running?}" ] + roles.map do |role_name, opts|
253
294
  "** #{role_name}: #{opts.inspect}"
@@ -266,8 +307,8 @@ module Awsymandias
266
307
  Awsymandias::SimpleDB.delete @sdb_domain, @name
267
308
  end
268
309
 
269
- def restore_from_role_to_instance_id_mapping
270
- @instances = returning(Awsymandias::SimpleDB.get(@sdb_domain, @name)) do |mapping|
310
+ def retrieve_role_to_instance_id_mapping
311
+ returning(Awsymandias::SimpleDB.get(@sdb_domain, @name)) do |mapping|
271
312
  unless mapping.empty?
272
313
  live_instances = Awsymandias::EC2::Instance.find(:all, :instance_ids => mapping.values.flatten).index_by(&:instance_id)
273
314
  mapping.each do |role_name, instance_id|
@@ -312,5 +353,5 @@ module Awsymandias
312
353
  returning(domain) { connection.create_domain(domain) unless domain_exists?(domain) }
313
354
  end
314
355
  end
315
- end
356
+ end
316
357
  end
@@ -18,11 +18,15 @@ describe Awsymandias do
18
18
  end
19
19
  end
20
20
 
21
- describe Awsymandias::EC2 do
21
+ describe Awsymandias::EC2 do
22
22
  def stub_connection_with(return_value)
23
23
  Awsymandias::EC2.stub!(:connection).and_return stub("a connection", :describe_instances => return_value)
24
24
  end
25
25
 
26
+ def zero_dollars
27
+ Money.new(0)
28
+ end
29
+
26
30
  describe "connection" do
27
31
  it "should configure an instance of EC2::Base" do
28
32
  Awsymandias.access_key_id = "configured key"
@@ -93,12 +97,12 @@ describe Awsymandias do
93
97
  "launchTime" => "2009-04-20T01:30:35.000Z",
94
98
  "instanceType" => "m1.large",
95
99
  "imageId" => "ami-some-image",
96
- "privateDnsName" => nil,
100
+ "privateDnsName" => "ip-10-244-226-239.ec2.internal",
97
101
  "reason" => nil,
98
102
  "placement" => {
99
103
  "availabilityZone" => "us-east-1c"
100
104
  },
101
- "dnsName" => nil,
105
+ "dnsName" => "ec2-174-129-118-52.compute-1.amazonaws.com",
102
106
  "instanceId" => "i-some-instance",
103
107
  "instanceState" => {
104
108
  "name" => "running",
@@ -110,55 +114,78 @@ describe Awsymandias do
110
114
  "requestId" => "7bca5c7c-1b51-473e-a930-611e55920e39",
111
115
  "xmlns"=>"http://ec2.amazonaws.com/doc/2008-12-01/",
112
116
  "reservationSet" => {
113
- "item" => [ {
114
- "reservationId" => "r-db68e3b2",
115
- "requesterId" => "058890971305",
116
- "ownerId" => "358110980006",
117
- "groupSet" => { "item" => [ { "groupId" => "default" } ] },
118
- "instancesSet" => { "item" => [
119
- {
120
- "productCodes" => nil,
121
- "kernelId" => "aki-some-kernel",
122
- "amiLaunchIndex" => "0",
123
- "keyName" => "gsg-keypair",
124
- "ramdiskId" => "ari-b31cf9da",
125
- "launchTime" => "2009-04-20T01:30:35.000Z",
126
- "instanceType" => "m1.large",
127
- "imageId" => "ami-some-image",
128
- "privateDnsName" => nil,
129
- "reason" => nil,
130
- "placement" => {
131
- "availabilityZone" => "us-east-1c"
132
- },
133
- "dnsName" => nil,
134
- "instanceId" => "i-some-instance",
135
- "instanceState" => {
136
- "name" => "running",
137
- "code"=>"0"
138
- }
139
- },
140
- {
141
- "productCodes" => nil,
142
- "kernelId" => "aki-some-kernel",
143
- "amiLaunchIndex" => "0",
144
- "keyName" => "gsg-keypair",
145
- "ramdiskId" => "ari-b31cf9da",
146
- "launchTime" => "2009-04-20T01:30:35.000Z",
147
- "instanceType" => "m1.large",
148
- "imageId" => "ami-some-image",
149
- "privateDnsName" => nil,
150
- "reason" => nil,
151
- "placement" => {
152
- "availabilityZone" => "us-east-1c"
153
- },
154
- "dnsName" => nil,
155
- "instanceId" => "i-another-instance",
156
- "instanceState" => {
157
- "name" => "pending",
158
- "code"=>"0"
159
- }
160
- } ] } } ] }
161
- }
117
+ "item" => [
118
+ { "reservationId"=>"r-5b226e32",
119
+ "ownerId"=>"423319072129",
120
+ "groupSet" => { "item" => [ {"groupId"=>"default" } ] },
121
+ "instancesSet" => { "item" => [
122
+ { "productCodes"=>nil,
123
+ "kernelId"=>"aki-some-kernel",
124
+ "amiLaunchIndex"=>"0",
125
+ "ramdiskId"=>"ari-b31cf9da",
126
+ "launchTime"=>"2009-07-14T17:47:33.000Z",
127
+ "instanceType"=>"c1.xlarge",
128
+ "imageId"=>"ami-some-other-image",
129
+ "privateDnsName"=>nil,
130
+ "reason"=>nil,
131
+ "placement" => {
132
+ "availabilityZone"=>"us-east-1b"
133
+ },
134
+ "dnsName" => nil,
135
+ "instanceId"=>"i-some-other-instance",
136
+ "instanceState" => {
137
+ "name"=>"running",
138
+ "code"=>"16",
139
+ }
140
+ }
141
+ ] } },
142
+ { "reservationId" => "r-db68e3b2",
143
+ "requesterId" => "058890971305",
144
+ "ownerId" => "358110980006",
145
+ "groupSet" => { "item" => [ { "groupId" => "default" } ] },
146
+ "instancesSet" => { "item" => [
147
+ { "productCodes" => nil,
148
+ "kernelId" => "aki-some-kernel",
149
+ "amiLaunchIndex" => "0",
150
+ "keyName" => "gsg-keypair",
151
+ "ramdiskId" => "ari-b31cf9da",
152
+ "launchTime" => "2009-04-20T01:30:35.000Z",
153
+ "instanceType" => "m1.large",
154
+ "imageId" => "ami-some-image",
155
+ "privateDnsName" => nil,
156
+ "reason" => nil,
157
+ "placement" => {
158
+ "availabilityZone" => "us-east-1c"
159
+ },
160
+ "dnsName" => nil,
161
+ "instanceId" => "i-some-instance",
162
+ "instanceState" => {
163
+ "name" => "running",
164
+ "code"=>"0"
165
+ } },
166
+ { "productCodes" => nil,
167
+ "kernelId" => "aki-some-kernel",
168
+ "amiLaunchIndex" => "0",
169
+ "keyName" => "gsg-keypair",
170
+ "ramdiskId" => "ari-b31cf9da",
171
+ "launchTime" => "2009-04-20T01:30:35.000Z",
172
+ "instanceType" => "m1.large",
173
+ "imageId" => "ami-some-image",
174
+ "privateDnsName" => nil,
175
+ "reason" => nil,
176
+ "placement" => {
177
+ "availabilityZone" => "us-east-1c"
178
+ },
179
+ "dnsName" => nil,
180
+ "instanceId" => "i-another-instance",
181
+ "instanceState" => {
182
+ "name" => "pending",
183
+ "code"=>"0"
184
+ }
185
+ } ] } }
186
+ ]
187
+ }
188
+ }
162
189
 
163
190
  RUN_INSTANCES_SINGLE_RESULT_XML = {
164
191
  "reservationId" => "r-276ee54e",
@@ -260,7 +287,9 @@ describe Awsymandias do
260
287
 
261
288
  it "should return more than one object if multiple IDs are requested" do
262
289
  stub_connection_with DESCRIBE_INSTANCES_MULTIPLE_RESULTS_RUNNING_XML
263
- Instance.find(:all, :instance_ids => ["i-some-instance", "i-another-instance"]).map(&:instance_id).should == [ "i-some-instance", "i-another-instance" ]
290
+ Instance.find(:all, :instance_ids => ["i-some-other-instance", "i-some-instance", "i-another-instance"]).map do |instance|
291
+ instance.instance_id
292
+ end.should == ["i-some-other-instance", "i-some-instance", "i-another-instance"]
264
293
  end
265
294
 
266
295
  it "should map camelized XML properties to Ruby-friendly underscored method names" do
@@ -391,11 +420,39 @@ describe Awsymandias do
391
420
  instance.uptime.should == (time_now - instance.launch_time)
392
421
  end
393
422
  end
423
+
424
+ describe "public_dns" do
425
+ it "should return the public dns from the xml" do
426
+ stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
427
+ Awsymandias::EC2::Instance.find("i-some-instance").public_dns.should == "ec2-174-129-118-52.compute-1.amazonaws.com"
428
+ end
429
+ end
430
+
431
+ describe "public_ip" do
432
+ it "should parse the public dns to get the public IP address" do
433
+ stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
434
+ Awsymandias::EC2::Instance.find("i-some-instance").public_ip.should == "174.129.118.52"
435
+ end
436
+ end
437
+
438
+ describe "private_dns" do
439
+ it "should return the private dns from the xml" do
440
+ stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
441
+ Awsymandias::EC2::Instance.find("i-some-instance").private_dns.should == "ip-10-244-226-239.ec2.internal"
442
+ end
443
+ end
444
+
445
+ describe "private_ip" do
446
+ it "should parse the private dns to get the private IP address" do
447
+ stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
448
+ Awsymandias::EC2::Instance.find("i-some-instance").private_ip.should == "10.244.226.239"
449
+ end
450
+ end
394
451
 
395
452
  describe "running_cost" do
396
453
  it "should be zero if the instance has not yet been launched" do
397
454
  stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_PENDING_XML
398
- Awsymandias::EC2::Instance.find("i-some-instance").running_cost.should == Money.new(0)
455
+ Awsymandias::EC2::Instance.find("i-some-instance").running_cost.should == zero_dollars
399
456
  end
400
457
 
401
458
  it "should be a single increment if the instance was launched 5 minutes ago" do
@@ -673,6 +730,23 @@ describe Awsymandias do
673
730
  Awsymandias::SimpleDB.get(ApplicationStack::DEFAULT_SDB_DOMAIN, "test").should be_blank
674
731
  end
675
732
  end
733
+
734
+ describe "running_cost" do
735
+ it "should be zero if the stack has not been launched" do
736
+ s = ApplicationStack.new("test") {|s| s.role "db1", :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE}
737
+ s.running_cost.should == zero_dollars
738
+ end
739
+
740
+ it "should be the sum total of the running cost of its constituent instances" do
741
+ stack = ApplicationStack.new "test"
742
+ stack.should_receive(:retrieve_role_to_instance_id_mapping).and_return({
743
+ :db => mock(:instance, :running_cost => Money.new(10)),
744
+ :app => mock(:instance, :running_cost => Money.new(20))
745
+ })
746
+
747
+ stack.running_cost.should == Money.new(30)
748
+ end
749
+ end
676
750
  end
677
751
  end
678
- end
752
+ end
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2007, 2008 Tim Dysinger
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.
@@ -0,0 +1 @@
1
+ Amazon SDB API
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'spec/rake/spectask'
3
+ require 'rake/gempackagetask'
4
+
5
+ Spec::Rake::SpecTask.new
6
+
7
+ gem_spec = eval(IO.read(File.join(File.dirname(__FILE__), "aws-sdb.gemspec")))
8
+
9
+ desc "Open an irb session preloaded with this library"
10
+ task :console do
11
+ sh "irb -rubygems -I lib -r aws_sdb.rb"
12
+ end
13
+
14
+ Rake::GemPackageTask.new(gem_spec) do |pkg|
15
+ pkg.gem_spec = gem_spec
16
+ end
17
+
18
+ task :install => [:package] do
19
+ sh %{sudo gem install pkg/#{gem_spec.name}-#{gem_spec.version}}
20
+ end
@@ -0,0 +1,3 @@
1
+ require 'aws_sdb/error'
2
+ require 'aws_sdb/service'
3
+
@@ -0,0 +1,42 @@
1
+ module AwsSdb
2
+
3
+ class Error < RuntimeError ; end
4
+
5
+ class RequestError < Error
6
+ attr_reader :request_id
7
+
8
+ def initialize(message, request_id=nil)
9
+ super(message)
10
+ @request_id = request_id
11
+ end
12
+ end
13
+
14
+ class InvalidDomainNameError < RequestError ; end
15
+ class InvalidParameterValueError < RequestError ; end
16
+ class InvalidNextTokenError < RequestError ; end
17
+ class InvalidNumberPredicatesError < RequestError ; end
18
+ class InvalidNumberValueTestsError < RequestError ; end
19
+ class InvalidQueryExpressionError < RequestError ; end
20
+ class MissingParameterError < RequestError ; end
21
+ class NoSuchDomainError < RequestError ; end
22
+ class NumberDomainsExceededError < RequestError ; end
23
+ class NumberDomainAttributesExceededError < RequestError ; end
24
+ class NumberDomainBytesExceededError < RequestError ; end
25
+ class NumberItemAttributesExceededError < RequestError ; end
26
+ class RequestTimeoutError < RequestError ; end
27
+
28
+ class FeatureDeprecatedError < RequestError ; end
29
+
30
+ class ConnectionError < Error
31
+ attr_reader :response
32
+
33
+ def initialize(response)
34
+ super(
35
+ "#{response.code} \
36
+ #{response.message if response.respond_to?(:message)}"
37
+ )
38
+ @response = response
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,191 @@
1
+ require 'logger'
2
+ require 'time'
3
+ require 'cgi'
4
+ require 'uri'
5
+ require 'net/http'
6
+ require 'base64'
7
+ require 'openssl'
8
+ require 'rexml/document'
9
+ require 'rexml/xpath'
10
+
11
+ module AwsSdb
12
+
13
+ class Service
14
+ def initialize(options={})
15
+ @access_key_id = options[:access_key_id] || ENV['AMAZON_ACCESS_KEY_ID']
16
+ @secret_access_key = options[:secret_access_key] || ENV['AMAZON_SECRET_ACCESS_KEY']
17
+ @base_url = options[:url] || 'http://sdb.amazonaws.com'
18
+ @logger = options[:logger] || Logger.new("aws_sdb.log")
19
+ end
20
+
21
+ def list_domains(max = nil, token = nil)
22
+ params = { 'Action' => 'ListDomains' }
23
+ params['NextToken'] =
24
+ token unless token.nil? || token.empty?
25
+ params['MaxNumberOfDomains'] =
26
+ max.to_s unless max.nil? || max.to_i == 0
27
+ doc = call(:get, params)
28
+ results = []
29
+ REXML::XPath.each(doc, '//DomainName/text()') do |domain|
30
+ results << domain.to_s
31
+ end
32
+ return results, REXML::XPath.first(doc, '//NextToken/text()').to_s
33
+ end
34
+
35
+ def create_domain(domain)
36
+ call(:post, { 'Action' => 'CreateDomain', 'DomainName'=> domain.to_s })
37
+ nil
38
+ end
39
+
40
+ def delete_domain(domain)
41
+ call(
42
+ :delete,
43
+ { 'Action' => 'DeleteDomain', 'DomainName' => domain.to_s }
44
+ )
45
+ nil
46
+ end
47
+ # <QueryWithAttributesResult><Item><Name>in-c2ffrw</Name><Attribute><Name>code</Name><Value>in-c2ffrw</Value></Attribute><Attribute><Name>date_created</Name><Value>2008-10-31</Value></Attribute></Item><Item>
48
+ def query_with_attributes(domain, query, max = nil, token = nil)
49
+ params = {
50
+ 'Action' => 'QueryWithAttributes',
51
+ 'QueryExpression' => query,
52
+ 'DomainName' => domain.to_s
53
+ }
54
+ params['NextToken'] =
55
+ token unless token.nil? || token.empty?
56
+ params['MaxNumberOfItems'] =
57
+ max.to_s unless max.nil? || max.to_i == 0
58
+
59
+ doc = call(:get, params)
60
+ results = []
61
+ REXML::XPath.each(doc, "//Item") do |item|
62
+ name = REXML::XPath.first(item, './Name/text()').to_s
63
+
64
+
65
+ attributes = {'Name' => name}
66
+ REXML::XPath.each(item, "./Attribute") do |attr|
67
+ key = REXML::XPath.first(attr, './Name/text()').to_s
68
+ value = REXML::XPath.first(attr, './Value/text()').to_s
69
+ ( attributes[key] ||= [] ) << value
70
+ end
71
+ results << attributes
72
+ end
73
+ return results, REXML::XPath.first(doc, '//NextToken/text()').to_s
74
+ end
75
+
76
+ # <QueryResult><ItemName>in-c2ffrw</ItemName><ItemName>in-72yagt</ItemName><ItemName>in-52j8gj</ItemName>
77
+ def query(domain, query, max = nil, token = nil)
78
+ params = {
79
+ 'Action' => 'Query',
80
+ 'QueryExpression' => query,
81
+ 'DomainName' => domain.to_s
82
+ }
83
+ params['NextToken'] =
84
+ token unless token.nil? || token.empty?
85
+ params['MaxNumberOfItems'] =
86
+ max.to_s unless max.nil? || max.to_i == 0
87
+
88
+
89
+ doc = call(:get, params)
90
+ results = []
91
+ REXML::XPath.each(doc, '//ItemName/text()') do |item|
92
+ results << item.to_s
93
+ end
94
+ return results, REXML::XPath.first(doc, '//NextToken/text()').to_s
95
+
96
+ end
97
+
98
+ def put_attributes(domain, item, attributes, replace = true)
99
+ params = {
100
+ 'Action' => 'PutAttributes',
101
+ 'DomainName' => domain.to_s,
102
+ 'ItemName' => item.to_s
103
+ }
104
+ count = 0
105
+ attributes.each do | key, values |
106
+ ([]<<values).flatten.each do |value|
107
+ params["Attribute.#{count}.Name"] = key.to_s
108
+ params["Attribute.#{count}.Value"] = value.to_s
109
+ params["Attribute.#{count}.Replace"] = replace
110
+ count += 1
111
+ end
112
+ end
113
+ call(:put, params)
114
+ nil
115
+ end
116
+
117
+ def get_attributes(domain, item)
118
+ doc = call(
119
+ :get,
120
+ {
121
+ 'Action' => 'GetAttributes',
122
+ 'DomainName' => domain.to_s,
123
+ 'ItemName' => item.to_s
124
+ }
125
+ )
126
+ attributes = {}
127
+ REXML::XPath.each(doc, "//Attribute") do |attr|
128
+ key = REXML::XPath.first(attr, './Name/text()').to_s
129
+ value = REXML::XPath.first(attr, './Value/text()').to_s
130
+ ( attributes[key] ||= [] ) << value
131
+ end
132
+ attributes
133
+ end
134
+
135
+ def delete_attributes(domain, item)
136
+ call(
137
+ :delete,
138
+ {
139
+ 'Action' => 'DeleteAttributes',
140
+ 'DomainName' => domain.to_s,
141
+ 'ItemName' => item.to_s
142
+ }
143
+ )
144
+ nil
145
+ end
146
+
147
+ protected
148
+
149
+ def call(method, params)
150
+ params.merge!( {
151
+ 'Version' => '2007-11-07',
152
+ 'SignatureVersion' => '1',
153
+ 'AWSAccessKeyId' => @access_key_id,
154
+ 'Timestamp' => Time.now.gmtime.iso8601
155
+ }
156
+ )
157
+ data = ''
158
+ query = []
159
+ params.keys.sort_by { |k| k.upcase }.each do |key|
160
+ data << "#{key}#{params[key].to_s}"
161
+ query << "#{key}=#{CGI::escape(params[key].to_s)}"
162
+ end
163
+ digest = OpenSSL::Digest::Digest.new('sha1')
164
+ hmac = OpenSSL::HMAC.digest(digest, @secret_access_key, data)
165
+ signature = Base64.encode64(hmac).strip
166
+ query << "Signature=#{CGI::escape(signature)}"
167
+ query = query.join('&')
168
+ url = "#{@base_url}?#{query}"
169
+ uri = URI.parse(url)
170
+ @logger.debug("#{url}") if @logger
171
+ response =
172
+ Net::HTTP.new(uri.host, uri.port).send_request(method, uri.request_uri)
173
+ @logger.debug("#{response.code}\n#{response.body}") if @logger
174
+ raise(ConnectionError.new(response)) unless (200..400).include?(
175
+ response.code.to_i
176
+ )
177
+ doc = REXML::Document.new(response.body)
178
+ error = doc.get_elements('*/Errors/Error')[0]
179
+ raise(
180
+ Module.class_eval(
181
+ "AwsSdb::#{error.get_elements('Code')[0].text}Error"
182
+ ).new(
183
+ error.get_elements('Message')[0].text,
184
+ doc.get_elements('*/RequestID')[0].text
185
+ )
186
+ ) unless error.nil?
187
+ doc
188
+ end
189
+ end
190
+
191
+ end
@@ -0,0 +1,183 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ require 'digest/sha1'
4
+ require 'net/http'
5
+ require 'rexml/document'
6
+
7
+ require 'rubygems'
8
+ require 'uuidtools'
9
+
10
+ include AwsSdb
11
+
12
+ describe Service, "when creating a new domain" do
13
+ before(:all) do
14
+ @service = AwsSdb::Service.new
15
+ @domain = "test-#{UUID.random_create.to_s}"
16
+ domains = @service.list_domains[0]
17
+ domains.each do |d|
18
+ @service.delete_domain(d) if d =~ /^test/
19
+ end
20
+ end
21
+
22
+ after(:all) do
23
+ @service.delete_domain(@domain)
24
+ end
25
+
26
+ it "should not raise an error if a valid new domain name is given" do
27
+ lambda {
28
+ @service.create_domain("test-#{UUID.random_create.to_s}")
29
+ }.should_not raise_error
30
+ end
31
+
32
+ it "should not raise an error if the domain name already exists" do
33
+ domain = "test-#{UUID.random_create.to_s}"
34
+ lambda {
35
+ @service.create_domain(domain)
36
+ @service.create_domain(domain)
37
+ }.should_not raise_error
38
+ end
39
+
40
+ it "should raise an error if an a nil or '' domain name is given" do
41
+ lambda {
42
+ @service.create_domain('')
43
+ }.should raise_error(InvalidParameterValueError)
44
+ lambda {
45
+ @service.create_domain(nil)
46
+ }.should raise_error(InvalidParameterValueError)
47
+ lambda {
48
+ @service.create_domain(' ')
49
+ }.should raise_error(InvalidParameterValueError)
50
+ end
51
+
52
+ it "should raise an error if the domain name length is < 3 or > 255" do
53
+ lambda {
54
+ @service.create_domain('xx')
55
+ }.should raise_error(InvalidParameterValueError)
56
+ lambda {
57
+ @service.create_domain('x'*256)
58
+ }.should raise_error(InvalidParameterValueError)
59
+ end
60
+
61
+ it "should only accept domain names with a-z, A-Z, 0-9, '_', '-', and '.' " do
62
+ lambda {
63
+ @service.create_domain('@$^*()')
64
+ }.should raise_error(InvalidParameterValueError)
65
+ end
66
+
67
+ it "should only accept a maximum of 100 domain names"
68
+
69
+ it "should not have to call amazon to determine domain name correctness"
70
+ end
71
+
72
+ describe Service, "when listing domains" do
73
+ before(:all) do
74
+ @service = AwsSdb::Service.new
75
+ @domain = "test-#{UUID.random_create.to_s}"
76
+ @service.list_domains[0].each do |d|
77
+ @service.delete_domain(d) if d =~ /^test/
78
+ end
79
+ @service.create_domain(@domain)
80
+ end
81
+
82
+ after(:all) do
83
+ @service.delete_domain(@domain)
84
+ end
85
+
86
+ it "should return a complete list" do
87
+ result = nil
88
+ lambda { result = @service.list_domains[0] }.should_not raise_error
89
+ result.should_not be_nil
90
+ result.should_not be_empty
91
+ result.include?(@domain).should == true
92
+ end
93
+ end
94
+
95
+ describe Service, "when deleting domains" do
96
+ before(:all) do
97
+ @service = AwsSdb::Service.new
98
+ @domain = "test-#{UUID.random_create.to_s}"
99
+ @service.list_domains[0].each do |d|
100
+ @service.delete_domain(d) if d =~ /^test/
101
+ end
102
+ @service.create_domain(@domain)
103
+ end
104
+
105
+ after do
106
+ @service.delete_domain(@domain)
107
+ end
108
+
109
+ it "should be able to delete an existing domain" do
110
+ lambda { @service.delete_domain(@domain) }.should_not raise_error
111
+ end
112
+
113
+ it "should not raise an error trying to delete a non-existing domain" do
114
+ lambda {
115
+ @service.delete_domain(UUID.random_create.to_s)
116
+ }.should_not raise_error
117
+ end
118
+ end
119
+
120
+ describe Service, "when managing items" do
121
+ before(:all) do
122
+ @service = AwsSdb::Service.new
123
+ @domain = "test-#{UUID.random_create.to_s}"
124
+ @service.list_domains[0].each do |d|
125
+ @service.delete_domain(d) if d =~ /^test/
126
+ end
127
+ @service.create_domain(@domain)
128
+ @item = "test-#{UUID.random_create.to_s}"
129
+ @attributes = {
130
+ :question => 'What is the answer?',
131
+ :answer => [ true, 'testing123', 4.2, 42, 420 ]
132
+ }
133
+ end
134
+
135
+ after(:all) do
136
+ @service.delete_domain(@domain)
137
+ end
138
+
139
+ it "should be able to put attributes" do
140
+ lambda {
141
+ @service.put_attributes(@domain, @item, @attributes)
142
+ }.should_not raise_error
143
+ end
144
+
145
+ it "should be able to get attributes" do
146
+ result = nil
147
+ lambda {
148
+ result = @service.get_attributes(@domain, @item)
149
+ }.should_not raise_error
150
+ result.should_not be_nil
151
+ result.should_not be_empty
152
+ result.has_key?('answer').should == true
153
+ @attributes[:answer].each do |v|
154
+ result['answer'].include?(v.to_s).should == true
155
+ end
156
+ end
157
+
158
+ it "should be able to query" do
159
+ result = nil
160
+ lambda {
161
+ result = @service.query(@domain, "[ 'answer' = '42' ]")[0]
162
+ }.should_not raise_error
163
+ result.should_not be_nil
164
+ result.should_not be_empty
165
+ result.should_not be_nil
166
+ result.include?(@item).should == true
167
+ end
168
+
169
+ it "should be able to query with attributes"
170
+
171
+ it "should be able to delete attributes" do
172
+ lambda {
173
+ @service.delete_attributes(@domain, @item)
174
+ }.should_not raise_error
175
+ end
176
+ end
177
+
178
+ # TODO Pull the specs from the amazon docs and write more rspec
179
+ # 100 attributes per each call
180
+ # 256 total attribute name-value pairs per item
181
+ # 250 million attributes per domain
182
+ # 10 GB of total user data storage per domain
183
+ # ...etc...
@@ -0,0 +1,4 @@
1
+ $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'aws_sdb'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bguthrie-awsymandias
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Guthrie
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-07 00:00:00 -07:00
12
+ date: 2009-07-14 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -33,24 +33,24 @@ dependencies:
33
33
  version: 2.3.0
34
34
  version:
35
35
  - !ruby/object:Gem::Dependency
36
- name: hungryblank-aws-sdb
36
+ name: grempe-amazon-ec2
37
37
  type: :runtime
38
38
  version_requirement:
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: 0.4.0
43
+ version: 0.4.2
44
44
  version:
45
45
  - !ruby/object:Gem::Dependency
46
- name: grempe-amazon-ec2
46
+ name: money
47
47
  type: :runtime
48
48
  version_requirement:
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 0.4.2
53
+ version: 2.1.3
54
54
  version:
55
55
  description: A library for helping you set up, track, and tear down complicated deployment configurations in Amazon EC2.
56
56
  email: btguthrie@gmail.com
@@ -65,8 +65,17 @@ files:
65
65
  - README.rdoc
66
66
  - Rakefile
67
67
  - VERSION
68
+ - awsymandias.gemspec
68
69
  - lib/awsymandias.rb
69
70
  - spec/awsymandias_spec.rb
71
+ - vendor/aws-sdb/LICENSE
72
+ - vendor/aws-sdb/README
73
+ - vendor/aws-sdb/Rakefile
74
+ - vendor/aws-sdb/lib/aws_sdb.rb
75
+ - vendor/aws-sdb/lib/aws_sdb/error.rb
76
+ - vendor/aws-sdb/lib/aws_sdb/service.rb
77
+ - vendor/aws-sdb/spec/aws_sdb/service_spec.rb
78
+ - vendor/aws-sdb/spec/spec_helper.rb
70
79
  has_rdoc: true
71
80
  homepage: http://github.com/bguthrie/awsymandias
72
81
  post_install_message: