rudy 0.2.4 → 0.3.0

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/CHANGES.txt CHANGED
@@ -4,8 +4,12 @@ RUDY, CHANGES
4
4
 
5
5
  NOTE: This is a complete re-write from 0.1
6
6
 
7
- * NEW: Commands: myaddress, addresses, images, instances, disks, connect, copy release
8
- * NEW: Metadata storage to SimpleDB
7
+ * NEW: All time references are converted to UTC
8
+ * NEW: Safer "Are you sure?". Number of characters to enter is
9
+ commiserate with amount of danger.
10
+ * NEW: Commands: myaddress, addresses, images, instances,
11
+ disks, connect, copy, stage, backups, volumes
12
+ * NEW: Metadata storage to SimpleDB for disks, backups
9
13
  * NEW: Creates EBS volumes based on startup from metadata
10
14
  * NEW: Automated release process
11
15
  * NEW: Automated creation of machine images
data/README.rdoc CHANGED
@@ -1,4 +1,4 @@
1
- = Rudy - v0.2 BETA!
1
+ = Rudy - v0.3 BETA!
2
2
 
3
3
  Rudy is a handy staging and deployment tool for EC2.
4
4
 
data/bin/rudy CHANGED
@@ -70,6 +70,18 @@ command :info => Rudy::Command::Metadata do |obj|
70
70
  obj.info
71
71
  end
72
72
 
73
+
74
+ option :e, :external, "Display only external IP address"
75
+ option :i, :internal, "Display only internal IP address"
76
+ usage "rudy myaddress [-i] [-e]"
77
+ command :myaddress do |obj|
78
+ ea = Rudy::Utils::external_ip_address
79
+ ia = Rudy::Utils::internal_ip_address
80
+ puts "%10s: %s" % ['Internal', ia] unless obj.external && !obj.internal
81
+ puts "%10s: %s" % ['External', ea] unless obj.internal && !obj.external
82
+ end
83
+
84
+
73
85
  option :D, :destroy, "Destroy all metadata stored in SimpleDB"
74
86
  option :u, :update, "Update the role or environment metadata for the given instance-IDs"
75
87
  usage "rudy [global options] metadata instance-ID"
@@ -81,7 +93,7 @@ command :metadata => Rudy::Command::Metadata do |obj, argv|
81
93
  raise "Nothing to change (see global options -r or -e)" unless obj.role || obj.environment
82
94
  obj.update_metadata(argv.first)
83
95
  elsif obj.destroy
84
- exit unless you_are_sure?
96
+ exit unless are_you_sure?
85
97
  obj.destroy_metadata
86
98
  else
87
99
  obj.print_metadata(argv.first)
@@ -113,7 +125,7 @@ command :connect => Rudy::Command::Environment do |obj|
113
125
  end
114
126
  end
115
127
 
116
- debug :on
128
+
117
129
  option :r, :remote, "Copy FROM the remote machine to the local machine"
118
130
  option :p, :print, "Only print the SSH command, don't connect"
119
131
  usage "rudy [-e env] [-u user] copy [-p] -r [from path] [to path]"
@@ -133,6 +145,7 @@ option :s, :size, Integer, "The size of disk (in GB)"
133
145
  option :C, :create, "Create a disk definition"
134
146
  option :D, :destroy, "Destroy a disk definition"
135
147
  option :A, :attach, "Attach a disk"
148
+ option :N, :unattach, "Unattach a disk"
136
149
  usage "rudy [global options] disks [-C -p path -d device -s size] [-A] [-D] [disk name]"
137
150
  command :disks => Rudy::Command::Disks do |obj, argv|
138
151
  capture(:stderr) do
@@ -143,8 +156,12 @@ command :disks => Rudy::Command::Disks do |obj, argv|
143
156
  obj.create_disk
144
157
  elsif obj.destroy
145
158
  raise "No disk specified" if argv.empty?
146
- exit unless you_are_sure?
159
+ exit unless are_you_sure?(5)
147
160
  obj.destroy_disk(argv.first)
161
+ elsif obj.unattach
162
+ raise "No disk specified" if argv.empty?
163
+ exit unless are_you_sure?(4)
164
+ obj.unattach_disk(argv.first)
148
165
  elsif obj.attach
149
166
  raise "No disk specified" if argv.empty?
150
167
  obj.attach_disk(argv.first)
@@ -155,9 +172,40 @@ command :disks => Rudy::Command::Disks do |obj, argv|
155
172
  end
156
173
  end
157
174
 
158
- #command :volumes => Rudy::Command::Volumes do |obj|
159
- # obj.print_volumes
160
- #end
175
+
176
+ #option :T, :tidy, "Tidy existing backups"
177
+ option :D, :destroy, "Destroy a backup"
178
+ option :C, :create, "Create a backup"
179
+ usage "rudy [global options] backups [-C] [disk name]"
180
+ command :backups => Rudy::Command::Disks do |obj, argv|
181
+ capture(:stderr) do
182
+ obj.print_header
183
+ if obj.create
184
+ obj.create_backup
185
+ elsif obj.destroy
186
+ raise "No backup specified" if argv.empty?
187
+ #exit unless are_you_sure?
188
+ obj.destroy_backup(argv.first)
189
+ else
190
+ obj.print_backups
191
+ end
192
+ end
193
+ end
194
+
195
+
196
+ option :D, :destroy, "Destroy a volume"
197
+ command :volumes => Rudy::Command::Volumes do |obj, argv|
198
+ capture(:stderr) do
199
+ obj.print_header
200
+
201
+ if obj.destroy
202
+ exit unless are_you_sure? 4
203
+ obj.destroy_volume(argv.first)
204
+ else
205
+ obj.print_volumes
206
+ end
207
+ end
208
+ end
161
209
 
162
210
  option :all, "Display all instances"
163
211
  option :a, :address, String, "Amazon elastic IP"
@@ -170,10 +218,10 @@ command :instances => Rudy::Command::Instances do |obj, argv|
170
218
  capture(:stderr) do
171
219
  obj.print_header
172
220
  if obj.destroy
173
- exit unless you_are_sure?
221
+ exit unless are_you_sure?
174
222
  obj.destroy_instances(argv.first)
175
223
  elsif obj.start
176
- exit unless you_are_sure?
224
+ exit unless are_you_sure?
177
225
  obj.start_instance
178
226
  else
179
227
  obj.print_instances(argv.first)
@@ -183,29 +231,22 @@ command :instances => Rudy::Command::Instances do |obj, argv|
183
231
  end
184
232
 
185
233
 
186
- option :e, :external, "Display only external IP address"
187
- option :i, :internal, "Display only internal IP address"
188
- usage "rudy myaddress [-i] [-e]"
189
- command :myaddress do |obj|
190
- ea = Rudy::Utils::external_ip_address
191
- ia = Rudy::Utils::internal_ip_address
192
- puts "%10s: %s" % ['Internal', ia] unless obj.external && !obj.internal
193
- puts "%10s: %s" % ['External', ea] unless obj.internal && !obj.external
194
- end
195
-
196
234
  option :a, :account, String, "Your Amazon Account Number"
197
235
  option :i, :image_name, String, "The name of the image"
198
236
  option :b, :bucket_name, String, "The name of the bucket that will store the image"
199
237
  option :C, :create, "Create an image"
200
- usage "rudy images [-C -i image -b bucket -a account]"
201
- command :images => Rudy::Command::Images do |obj|
238
+ option :D, :destroy, "Deregister an image"
239
+ usage "rudy images [-C -i name -b bucket -a account] [-D AMI-ID]"
240
+ command :images => Rudy::Command::Images do |obj, argv|
202
241
  capture(:stderr) do
203
242
  obj.print_header
204
-
243
+
205
244
  if obj.create
206
245
  puts "Make sure the machine is clean. I don't want archive no crud!"
207
- exit unless you_are_sure?
246
+ exit unless are_you_sure?
208
247
  obj.create_image
248
+ elsif obj.destroy
249
+ obj.deregister(argv.first)
209
250
  else
210
251
  obj.print_images
211
252
  end
@@ -218,7 +259,7 @@ command :stage => Rudy::Command::Stage do |obj|
218
259
 
219
260
  raise "No SCM defined. Set RUDY_SVN_BASE or RUDY_GIT_BASE." unless obj.scm
220
261
 
221
- exit unless you_are_sure?
262
+ exit unless are_you_sure?
222
263
  obj.push_to_stage
223
264
  end
224
265
  end
@@ -245,7 +286,7 @@ command :groups => Rudy::Command::Groups do |obj, argv|
245
286
  obj.modify_group(argv.first)
246
287
 
247
288
  elsif obj.destroy
248
- exit unless you_are_sure?
289
+ exit unless are_you_sure?
249
290
  obj.destroy_group(argv.first)
250
291
 
251
292
  else
@@ -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,215 @@
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
+ def select(select, token = nil)
148
+ params = {
149
+ 'Action' => 'Select',
150
+ 'SelectExpression' => select,
151
+ }
152
+ params['NextToken'] =
153
+ token unless token.nil? || token.empty?
154
+
155
+ doc = call(:get, params)
156
+ results = []
157
+ REXML::XPath.each(doc, "//Item") do |item|
158
+ name = REXML::XPath.first(item, './Name/text()').to_s
159
+
160
+ attributes = {'Name' => name}
161
+ REXML::XPath.each(item, "./Attribute") do |attr|
162
+ key = REXML::XPath.first(attr, './Name/text()').to_s
163
+ value = REXML::XPath.first(attr, './Value/text()').to_s
164
+ ( attributes[key] ||= [] ) << value
165
+ end
166
+ results << attributes
167
+ end
168
+ return results, REXML::XPath.first(doc, '//NextToken/text()').to_s
169
+ end
170
+
171
+ protected
172
+
173
+ def call(method, params)
174
+ params.merge!( {
175
+ 'Version' => '2007-11-07',
176
+ 'SignatureVersion' => '1',
177
+ 'AWSAccessKeyId' => @access_key_id,
178
+ 'Timestamp' => Time.now.gmtime.iso8601
179
+ }
180
+ )
181
+ data = ''
182
+ query = []
183
+ params.keys.sort_by { |k| k.upcase }.each do |key|
184
+ data << "#{key}#{params[key].to_s}"
185
+ query << "#{key}=#{CGI::escape(params[key].to_s)}"
186
+ end
187
+ digest = OpenSSL::Digest::Digest.new('sha1')
188
+ hmac = OpenSSL::HMAC.digest(digest, @secret_access_key, data)
189
+ signature = Base64.encode64(hmac).strip
190
+ query << "Signature=#{CGI::escape(signature)}"
191
+ query = query.join('&')
192
+ url = "#{@base_url}?#{query}"
193
+ uri = URI.parse(url)
194
+ @logger.debug("#{url}") if @logger
195
+ response =
196
+ Net::HTTP.new(uri.host, uri.port).send_request(method, uri.request_uri)
197
+ @logger.debug("#{response.code}\n#{response.body}") if @logger
198
+ raise(ConnectionError.new(response)) unless (200..400).include?(
199
+ response.code.to_i
200
+ )
201
+ doc = REXML::Document.new(response.body)
202
+ error = doc.get_elements('*/Errors/Error')[0]
203
+ raise(
204
+ Module.class_eval(
205
+ "AwsSdb::#{error.get_elements('Code')[0].text}Error"
206
+ ).new(
207
+ error.get_elements('Message')[0].text,
208
+ doc.get_elements('*/RequestID')[0].text
209
+ )
210
+ ) unless error.nil?
211
+ doc
212
+ end
213
+ end
214
+
215
+ end
data/lib/aws_sdb.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'aws_sdb/error'
2
+ require 'aws_sdb/service'
3
+