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 +6 -2
- data/README.rdoc +1 -1
- data/bin/rudy +65 -24
- data/lib/aws_sdb/error.rb +42 -0
- data/lib/aws_sdb/service.rb +215 -0
- data/lib/aws_sdb.rb +3 -0
- data/lib/console.rb +341 -0
- data/lib/rudy/aws/ec2.rb +45 -0
- data/lib/rudy/aws/simpledb.rb +5 -0
- data/lib/rudy/aws.rb +3 -0
- data/lib/rudy/command/base.rb +47 -23
- data/lib/rudy/command/disks.rb +109 -2
- data/lib/rudy/command/environment.rb +4 -12
- data/lib/rudy/command/images.rb +15 -2
- data/lib/rudy/command/stage.rb +1 -1
- data/lib/rudy/command/volumes.rb +38 -1
- data/lib/rudy/metadata/backup.rb +160 -0
- data/lib/rudy/metadata/disk.rb +54 -23
- data/lib/rudy/metadata/ec2startup.rb +2 -0
- data/lib/rudy/metadata.rb +26 -0
- data/lib/rudy.rb +38 -7
- data/lib/storable.rb +20 -15
- data/rudy.gemspec +8 -1
- metadata +9 -2
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:
|
8
|
-
* NEW:
|
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
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
|
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
|
-
|
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
|
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
|
-
|
159
|
-
#
|
160
|
-
|
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
|
221
|
+
exit unless are_you_sure?
|
174
222
|
obj.destroy_instances(argv.first)
|
175
223
|
elsif obj.start
|
176
|
-
exit unless
|
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
|
-
|
201
|
-
|
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
|
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
|
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
|
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