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