rudy 0.2.4 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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