solutious-rudy 0.4.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 +75 -0
- data/LICENSE.txt +19 -0
- data/README.rdoc +36 -0
- data/Rakefile +68 -0
- data/bin/rudy +175 -0
- data/bin/rudy-ec2 +108 -0
- data/lib/aws_sdb.rb +3 -0
- data/lib/aws_sdb/error.rb +42 -0
- data/lib/aws_sdb/service.rb +215 -0
- data/lib/console.rb +385 -0
- data/lib/rudy.rb +210 -0
- data/lib/rudy/aws.rb +68 -0
- data/lib/rudy/aws/ec2.rb +304 -0
- data/lib/rudy/aws/s3.rb +3 -0
- data/lib/rudy/aws/simpledb.rb +53 -0
- data/lib/rudy/command/addresses.rb +46 -0
- data/lib/rudy/command/backups.rb +175 -0
- data/lib/rudy/command/base.rb +839 -0
- data/lib/rudy/command/config.rb +77 -0
- data/lib/rudy/command/deploy.rb +12 -0
- data/lib/rudy/command/disks.rb +213 -0
- data/lib/rudy/command/environment.rb +74 -0
- data/lib/rudy/command/groups.rb +61 -0
- data/lib/rudy/command/images.rb +99 -0
- data/lib/rudy/command/instances.rb +85 -0
- data/lib/rudy/command/machines.rb +170 -0
- data/lib/rudy/command/metadata.rb +41 -0
- data/lib/rudy/command/release.rb +174 -0
- data/lib/rudy/command/volumes.rb +66 -0
- data/lib/rudy/config.rb +93 -0
- data/lib/rudy/metadata.rb +26 -0
- data/lib/rudy/metadata/backup.rb +160 -0
- data/lib/rudy/metadata/disk.rb +138 -0
- data/lib/rudy/scm/svn.rb +68 -0
- data/lib/rudy/utils.rb +64 -0
- data/lib/storable.rb +280 -0
- data/lib/tryouts.rb +40 -0
- data/rudy.gemspec +76 -0
- data/support/mailtest +40 -0
- data/support/rudy-ec2-startup +200 -0
- data/tryouts/console_tryout.rb +91 -0
- metadata +188 -0
data/lib/rudy/scm/svn.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Rudy
|
5
|
+
module SCM
|
6
|
+
class SVN
|
7
|
+
attr_accessor :base_uri
|
8
|
+
|
9
|
+
def initialize(args={:base => ''})
|
10
|
+
@base_uri = args[:base]
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_release(username=nil, msg=nil)
|
14
|
+
local_uri, local_revision = local_info
|
15
|
+
rtag = generate_release_tag_name(username)
|
16
|
+
release_uri = "#{@base_uri}/#{rtag}"
|
17
|
+
msg ||= 'Another Release by Rudy!'
|
18
|
+
msg.tr!("'", "\\'")
|
19
|
+
cmd = "svn copy -m '#{msg}' #{local_uri} #{release_uri}"
|
20
|
+
|
21
|
+
`#{cmd} 2>&1`
|
22
|
+
|
23
|
+
release_uri
|
24
|
+
end
|
25
|
+
|
26
|
+
def switch_working_copy(tag)
|
27
|
+
raise "Invalid release tag (#{tag})." unless valid_uri?(tag)
|
28
|
+
`svn switch #{tag}`
|
29
|
+
end
|
30
|
+
|
31
|
+
# rel-2009-03-05-user-rev
|
32
|
+
def generate_release_tag_name(username=nil)
|
33
|
+
now = Time.now
|
34
|
+
mon = now.mon.to_s.rjust(2, '0')
|
35
|
+
day = now.day.to_s.rjust(2, '0')
|
36
|
+
rev = "01"
|
37
|
+
criteria = ['rel', now.year, mon, day, rev]
|
38
|
+
criteria.insert(-2, username) if username
|
39
|
+
tag = criteria.join(RUDY_DELIM)
|
40
|
+
# Keep incrementing the revision number until we find the next one.
|
41
|
+
tag.succ! while (valid_uri?("#{@base_uri}/#{tag}"))
|
42
|
+
tag
|
43
|
+
end
|
44
|
+
|
45
|
+
def local_info
|
46
|
+
ret = `svn info 2>&1`
|
47
|
+
# URL: http://some/uri/path
|
48
|
+
# Repository Root: http://some/uri
|
49
|
+
# Repository UUID: c5abe49d-53e4-4ea3-9314-89e1e25aa7e1
|
50
|
+
# Revision: 921
|
51
|
+
ret.scan(/URL: (http:.+?)\s*\n.+Revision: (\d+)/m).flatten
|
52
|
+
end
|
53
|
+
|
54
|
+
def working_copy?(path)
|
55
|
+
(File.exists?(File.join(path, '.svn')))
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_uri?(uri)
|
59
|
+
ret = `svn info #{uri} 2>&1` || '' # Valid SVN URIs will return some info
|
60
|
+
(ret =~ /Repository UUID/) ? true : false
|
61
|
+
end
|
62
|
+
|
63
|
+
def everything_checked_in?
|
64
|
+
`svn diff . 2>&1` == '' # svn diff should return nothing
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/rudy/utils.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
|
2
|
+
require 'socket'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
require 'timeout'
|
7
|
+
|
8
|
+
module Rudy
|
9
|
+
|
10
|
+
# A motley collection of methods that Rudy loves to call!
|
11
|
+
module Utils
|
12
|
+
extend self
|
13
|
+
include Socket::Constants
|
14
|
+
|
15
|
+
# Return the external IP address (the one seen by the internet)
|
16
|
+
def external_ip_address
|
17
|
+
ip = nil
|
18
|
+
%w{solutious.com/ip myip.dk/ whatismyip.com }.each do |sponge| # w/ backup
|
19
|
+
break unless ip.nil?
|
20
|
+
ip = (open("http://#{sponge}") { |f| /([0-9]{1,3}\.){3}[0-9]{1,3}/.match(f.read) }).to_s rescue nil
|
21
|
+
end
|
22
|
+
ip += "/32" if ip
|
23
|
+
ip
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the local IP address which receives external traffic
|
27
|
+
# from: http://coderrr.wordpress.com/2008/05/28/get-your-local-ip-address/
|
28
|
+
# NOTE: This <em>does not</em> open a connection to the IP address.
|
29
|
+
def internal_ip_address
|
30
|
+
# turn off reverse DNS resolution temporarily
|
31
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
|
32
|
+
ip = UDPSocket.open {|s| s.connect('75.101.137.7', 1); s.addr.last } # Solutious IP
|
33
|
+
ip += "/24" if ip
|
34
|
+
ip
|
35
|
+
ensure
|
36
|
+
Socket.do_not_reverse_lookup = orig
|
37
|
+
end
|
38
|
+
|
39
|
+
# Generates a canonical tag name in the form:
|
40
|
+
# rudy-2009-12-31-r1
|
41
|
+
# where r1 refers to the revision number that day
|
42
|
+
def generate_tag(revision=1)
|
43
|
+
n = DateTime.now
|
44
|
+
y = n.year.to_s.rjust(4, "20")
|
45
|
+
m = n.month.to_s.rjust(2, "0")
|
46
|
+
d = n.mday.to_s.rjust(2, "0")
|
47
|
+
"rudy-%4s-%2s-%2s-r%s" % [y, m, d, revision.to_s.rjust(2, "0")]
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def service_available?(host, port, wait=3)
|
52
|
+
begin
|
53
|
+
status = Timeout::timeout(wait) do
|
54
|
+
socket = Socket.new( AF_INET, SOCK_STREAM, 0 )
|
55
|
+
sockaddr = Socket.pack_sockaddr_in( port, host )
|
56
|
+
socket.connect( sockaddr )
|
57
|
+
end
|
58
|
+
true
|
59
|
+
rescue Errno::EAFNOSUPPORT, Errno::ECONNREFUSED, SocketError, Timeout::Error => ex
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/storable.rb
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
#--
|
2
|
+
# TODO: Handle nested hashes and arrays.
|
3
|
+
# TODO: to_xml, see: http://codeforpeople.com/lib/ruby/xx/xx-2.0.0/README
|
4
|
+
# TODO: Rename to Stuffany
|
5
|
+
#++
|
6
|
+
|
7
|
+
require 'yaml'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
|
11
|
+
# Storable makes data available in multiple formats and can
|
12
|
+
# re-create objects from files. Fields are defined using the
|
13
|
+
# Storable.field method which tells Storable the order and
|
14
|
+
# name.
|
15
|
+
class Storable
|
16
|
+
VERSION = 2
|
17
|
+
NICE_TIME_FORMAT = "%Y-%m-%d@%H:%M:%S".freeze unless defined? NICE_TIME_FORMAT
|
18
|
+
SUPPORTED_FORMATS = %w{tsv csv yaml json}.freeze unless defined? SUPPORTED_FORMATS
|
19
|
+
|
20
|
+
# This value will be used as a default unless provided on-the-fly.
|
21
|
+
# See SUPPORTED_FORMATS for available values.
|
22
|
+
attr_reader :format
|
23
|
+
|
24
|
+
# See SUPPORTED_FORMATS for available values
|
25
|
+
def format=(v)
|
26
|
+
raise "Unsupported format: #{v}" unless SUPPORTED_FORMATS.member?(v)
|
27
|
+
@format = v
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: from_args([HASH or ordered params])
|
31
|
+
|
32
|
+
def init
|
33
|
+
# NOTE: I think this can be removed
|
34
|
+
self.class.send(:class_variable_set, :@@field_names, []) unless class_variable_defined?(:@@field_names)
|
35
|
+
self.class.send(:class_variable_set, :@@field_types, []) unless class_variable_defined?(:@@field_types)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Accepts field definitions in the one of the follow formats:
|
39
|
+
#
|
40
|
+
# field :product
|
41
|
+
# field :product => Integer
|
42
|
+
#
|
43
|
+
# The order they're defined determines the order the will be output. The fields
|
44
|
+
# data is available by the standard accessors, class.product and class.product= etc...
|
45
|
+
# The value of the field will be cast to the type (if provided) when read from a file.
|
46
|
+
# The value is not touched when the type is not provided.
|
47
|
+
def self.field(args={})
|
48
|
+
# TODO: Examine casting from: http://codeforpeople.com/lib/ruby/fattr/fattr-1.0.3/
|
49
|
+
args = {args => nil} unless args.is_a? Hash
|
50
|
+
|
51
|
+
args.each_pair do |m,t|
|
52
|
+
|
53
|
+
[[:@@field_names, m], [:@@field_types, t]].each do |tuple|
|
54
|
+
class_variable_set(tuple[0], []) unless class_variable_defined?(tuple[0])
|
55
|
+
class_variable_set(tuple[0], class_variable_get(tuple[0]) << tuple[1])
|
56
|
+
end
|
57
|
+
|
58
|
+
next if method_defined?(m)
|
59
|
+
|
60
|
+
define_method(m) do instance_variable_get("@#{m}") end
|
61
|
+
define_method("#{m}=") do |val|
|
62
|
+
instance_variable_set("@#{m}",val)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns an array of field names defined by self.field
|
68
|
+
def self.field_names
|
69
|
+
class_variable_get(:@@field_names)
|
70
|
+
end
|
71
|
+
# Ditto.
|
72
|
+
def field_names
|
73
|
+
self.class.send(:class_variable_get, :@@field_names)
|
74
|
+
end
|
75
|
+
# Returns an array of field types defined by self.field. Fields that did
|
76
|
+
# not receive a type are set to nil.
|
77
|
+
def self.field_types
|
78
|
+
class_variable_get(:@@field_types)
|
79
|
+
end
|
80
|
+
# Ditto.
|
81
|
+
def field_types
|
82
|
+
self.class.send(:class_variable_get, :@@field_types)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Dump the object data to the given format.
|
86
|
+
def dump(format=nil, with_titles=true)
|
87
|
+
format ||= @format
|
88
|
+
raise "Format not defined (#{format})" unless SUPPORTED_FORMATS.member?(format)
|
89
|
+
send("to_#{format}", with_titles)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Create a new instance of the object using data from file.
|
93
|
+
def self.from_file(file_path, format='yaml')
|
94
|
+
raise "Cannot read file (#{file_path})" unless File.exists?(file_path)
|
95
|
+
raise "#{self} doesn't support from_#{format}" unless self.respond_to?("from_#{format}")
|
96
|
+
format = format || File.extname(file_path).tr('.', '')
|
97
|
+
me = send("from_#{format}", read_file_to_array(file_path))
|
98
|
+
me.format = format
|
99
|
+
me
|
100
|
+
end
|
101
|
+
# Write the object data to the given file.
|
102
|
+
def to_file(file_path=nil, with_titles=true)
|
103
|
+
raise "Cannot store to nil path" if file_path.nil?
|
104
|
+
format = File.extname(file_path).tr('.', '')
|
105
|
+
format ||= @format
|
106
|
+
Storable.write_file(file_path, dump(format, with_titles))
|
107
|
+
end
|
108
|
+
|
109
|
+
# Create a new instance of the object from a hash.
|
110
|
+
def self.from_hash(from={})
|
111
|
+
me = self.new
|
112
|
+
|
113
|
+
return me if !from || from.empty?
|
114
|
+
|
115
|
+
fnames = field_names
|
116
|
+
fnames.each_with_index do |key,index|
|
117
|
+
|
118
|
+
stored_value = from[key] || from[key.to_s] # support for symbol keys and string keys
|
119
|
+
|
120
|
+
# TODO: Correct this horrible implementation (sorry, me. It's just one of those days.)
|
121
|
+
|
122
|
+
if field_types[index] == Array
|
123
|
+
((value ||= []) << stored_value).flatten
|
124
|
+
elsif field_types[index] == Hash
|
125
|
+
value = stored_value
|
126
|
+
else
|
127
|
+
# SimpleDB stores attribute shit as lists of values
|
128
|
+
value = stored_value.first if stored_value.is_a?(Array) && stored_value.size == 1
|
129
|
+
|
130
|
+
if field_types[index] == Time
|
131
|
+
value = Time.parse(value)
|
132
|
+
elsif field_types[index] == DateTime
|
133
|
+
value = DateTime.parse(value)
|
134
|
+
elsif field_types[index] == TrueClass
|
135
|
+
value = (value.to_s == "true")
|
136
|
+
elsif field_types[index] == Float
|
137
|
+
value = value.to_f
|
138
|
+
elsif field_types[index] == Integer
|
139
|
+
value = value.to_i
|
140
|
+
else
|
141
|
+
value = value.first if value.is_a?(Array) && value.size == 1 # I
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
me.send("#{key}=", value) if self.method_defined?("#{key}=")
|
146
|
+
end
|
147
|
+
|
148
|
+
me
|
149
|
+
end
|
150
|
+
# Return the object data as a hash
|
151
|
+
# +with_titles+ is ignored.
|
152
|
+
def to_hash(with_titles=true)
|
153
|
+
tmp = {}
|
154
|
+
field_names.each do |fname|
|
155
|
+
tmp[fname] = self.send(fname)
|
156
|
+
end
|
157
|
+
tmp
|
158
|
+
end
|
159
|
+
|
160
|
+
# Create a new instance of the object from YAML.
|
161
|
+
# +from+ a YAML string split into an array by line.
|
162
|
+
def self.from_yaml(from=[])
|
163
|
+
# from is an array of strings
|
164
|
+
from_str = from.join('')
|
165
|
+
hash = YAML::load(from_str)
|
166
|
+
hash = from_hash(hash) if hash.is_a? Hash
|
167
|
+
hash
|
168
|
+
end
|
169
|
+
def to_yaml(with_titles=true)
|
170
|
+
to_hash.to_yaml
|
171
|
+
end
|
172
|
+
|
173
|
+
# Create a new instance of the object from a JSON string.
|
174
|
+
# +from+ a JSON string split into an array by line.
|
175
|
+
def self.from_json(from=[])
|
176
|
+
require 'json'
|
177
|
+
# from is an array of strings
|
178
|
+
from_str = from.join('')
|
179
|
+
tmp = JSON::load(from_str)
|
180
|
+
hash_sym = tmp.keys.inject({}) do |hash, key|
|
181
|
+
hash[key.to_sym] = tmp[key]
|
182
|
+
hash
|
183
|
+
end
|
184
|
+
hash_sym = from_hash(hash_sym) if hash_sym.is_a? Hash
|
185
|
+
hash_sym
|
186
|
+
end
|
187
|
+
def to_json(with_titles=true)
|
188
|
+
require 'json'
|
189
|
+
to_hash.to_json
|
190
|
+
end
|
191
|
+
|
192
|
+
# Return the object data as a delimited string.
|
193
|
+
# +with_titles+ specifiy whether to include field names (default: false)
|
194
|
+
# +delim+ is the field delimiter.
|
195
|
+
def to_delimited(with_titles=false, delim=',')
|
196
|
+
values = []
|
197
|
+
field_names.each do |fname|
|
198
|
+
values << self.send(fname.to_s) # TODO: escape values
|
199
|
+
end
|
200
|
+
output = values.join(delim)
|
201
|
+
output = field_names.join(delim) << $/ << output if with_titles
|
202
|
+
output
|
203
|
+
end
|
204
|
+
# Return the object data as a tab delimited string.
|
205
|
+
# +with_titles+ specifiy whether to include field names (default: false)
|
206
|
+
def to_tsv(with_titles=false)
|
207
|
+
to_delimited(with_titles, "\t")
|
208
|
+
end
|
209
|
+
# Return the object data as a comma delimited string.
|
210
|
+
# +with_titles+ specifiy whether to include field names (default: false)
|
211
|
+
def to_csv(with_titles=false)
|
212
|
+
to_delimited(with_titles, ',')
|
213
|
+
end
|
214
|
+
# Create a new instance from tab-delimited data.
|
215
|
+
# +from+ a JSON string split into an array by line.
|
216
|
+
def self.from_tsv(from=[])
|
217
|
+
self.from_delimited(from, "\t")
|
218
|
+
end
|
219
|
+
# Create a new instance of the object from comma-delimited data.
|
220
|
+
# +from+ a JSON string split into an array by line.
|
221
|
+
def self.from_csv(from=[])
|
222
|
+
self.from_delimited(from, ',')
|
223
|
+
end
|
224
|
+
|
225
|
+
# Create a new instance of the object from a delimited string.
|
226
|
+
# +from+ a JSON string split into an array by line.
|
227
|
+
# +delim+ is the field delimiter.
|
228
|
+
def self.from_delimited(from=[],delim=',')
|
229
|
+
return if from.empty?
|
230
|
+
# We grab an instance of the class so we can
|
231
|
+
hash = {}
|
232
|
+
|
233
|
+
fnames = values = []
|
234
|
+
if (from.size > 1 && !from[1].empty?)
|
235
|
+
fnames = from[0].chomp.split(delim)
|
236
|
+
values = from[1].chomp.split(delim)
|
237
|
+
else
|
238
|
+
fnames = self.field_names
|
239
|
+
values = from[0].chomp.split(delim)
|
240
|
+
end
|
241
|
+
|
242
|
+
fnames.each_with_index do |key,index|
|
243
|
+
next unless values[index]
|
244
|
+
hash[key.to_sym] = values[index]
|
245
|
+
end
|
246
|
+
hash = from_hash(hash) if hash.is_a? Hash
|
247
|
+
hash
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.read_file_to_array(path)
|
251
|
+
contents = []
|
252
|
+
return contents unless File.exists?(path)
|
253
|
+
|
254
|
+
open(path, 'r') do |l|
|
255
|
+
contents = l.readlines
|
256
|
+
end
|
257
|
+
|
258
|
+
contents
|
259
|
+
end
|
260
|
+
|
261
|
+
def self.write_file(path, content, flush=true)
|
262
|
+
write_or_append_file('w', path, content, flush)
|
263
|
+
end
|
264
|
+
|
265
|
+
def self.append_file(path, content, flush=true)
|
266
|
+
write_or_append_file('a', path, content, flush)
|
267
|
+
end
|
268
|
+
|
269
|
+
def self.write_or_append_file(write_or_append, path, content = '', flush = true)
|
270
|
+
#STDERR.puts "Writing to #{ path }..."
|
271
|
+
create_dir(File.dirname(path))
|
272
|
+
|
273
|
+
open(path, write_or_append) do |f|
|
274
|
+
f.puts content
|
275
|
+
f.flush if flush;
|
276
|
+
end
|
277
|
+
File.chmod(0600, path)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
data/lib/tryouts.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module Tryouts
|
5
|
+
|
6
|
+
def before(&b)
|
7
|
+
b.call
|
8
|
+
end
|
9
|
+
def after(&b)
|
10
|
+
at_exit &b
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
# tryout :name do
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
def tryout(name, &b)
|
18
|
+
puts "Running#{@poop}: #{name}"
|
19
|
+
begin
|
20
|
+
b.call
|
21
|
+
puts $/*2
|
22
|
+
sleep 1
|
23
|
+
rescue Interrupt
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Ignore everything
|
28
|
+
def xtryout(name, &b)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Is this wacky syntax useful for anything?
|
32
|
+
# t2 :set .
|
33
|
+
# run = "poop"
|
34
|
+
def t2(*args)
|
35
|
+
OpenStruct.new
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
include Tryouts
|