rudy 0.2

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.
@@ -0,0 +1,65 @@
1
+
2
+ require 'socket'
3
+ require 'open-uri'
4
+ require 'date'
5
+
6
+ require 'socket'
7
+ require 'timeout'
8
+
9
+ module Rudy
10
+
11
+ # A motley collection of methods that Rudy loves to call!
12
+ module Utils
13
+ extend self
14
+ include Socket::Constants
15
+
16
+ # Return the external IP address (the one seen by the internet)
17
+ def external_ip_address
18
+ ip = nil
19
+ %w{solutious.com/ip myip.dk/ whatismyip.com }.each do |sponge| # w/ backup
20
+ break unless ip.nil?
21
+ ip = (open("http://#{sponge}") { |f| /([0-9]{1,3}\.){3}[0-9]{1,3}/.match(f.read) }).to_s rescue nil
22
+ end
23
+ ip += "/32" if ip
24
+ ip
25
+ end
26
+
27
+ # Return the local IP address which receives external traffic
28
+ # from: http://coderrr.wordpress.com/2008/05/28/get-your-local-ip-address/
29
+ # NOTE: This <em>does not</em> open a connection to the IP address.
30
+ def internal_ip_address
31
+ # turn off reverse DNS resolution temporarily
32
+ orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
33
+ ip = UDPSocket.open {|s| s.connect('75.101.137.7', 1); s.addr.last } # Solutious IP
34
+ ip += "/24" if ip
35
+ ip
36
+ ensure
37
+ Socket.do_not_reverse_lookup = orig
38
+ end
39
+
40
+ # Generates a canonical tag name in the form:
41
+ # rudy-2009-12-31-r1
42
+ # where r1 refers to the revision number that day
43
+ def generate_tag(revision=1)
44
+ n = DateTime.now
45
+ y = n.year.to_s.rjust(4, "20")
46
+ m = n.month.to_s.rjust(2, "0")
47
+ d = n.mday.to_s.rjust(2, "0")
48
+ "rudy-%4s-%2s-%2s-r%s" % [y, m, d, revision.to_s.rjust(2, "0")]
49
+ end
50
+
51
+
52
+ def service_available?(host, port)
53
+ begin
54
+ status = Timeout::timeout(3) do
55
+ socket = Socket.new( AF_INET, SOCK_STREAM, 0 )
56
+ sockaddr = Socket.pack_sockaddr_in( port, host )
57
+ socket.connect( sockaddr )
58
+ end
59
+ true
60
+ rescue Errno::EAFNOSUPPORT, Errno::ECONNREFUSED, SocketError, Timeout::Error => ex
61
+ false
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,268 @@
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
+ #++
5
+
6
+ require 'yaml'
7
+ require 'fileutils'
8
+
9
+
10
+ # Storable makes data available in multiple formats and can
11
+ # re-create objects from files. Fields are defined using the
12
+ # Storable.field method which tells Storable the order and
13
+ # name.
14
+ class Storable
15
+ NICE_TIME_FORMAT = "%Y-%m-%d@%H:%M:%S".freeze unless defined? NICE_TIME_FORMAT
16
+ SUPPORTED_FORMATS = %w{tsv csv yaml json}.freeze unless defined? SUPPORTED_FORMATS
17
+
18
+ # This value will be used as a default unless provided on-the-fly.
19
+ # See SUPPORTED_FORMATS for available values.
20
+ attr_reader :format
21
+
22
+ # See SUPPORTED_FORMATS for available values
23
+ def format=(v)
24
+ raise "Unsupported format: #{v}" unless SUPPORTED_FORMATS.member?(v)
25
+ @format = v
26
+ end
27
+
28
+ def init
29
+ self.class.send(:class_variable_set, :@@field_names, []) unless class_variable_defined?(:@@field_names)
30
+ self.class.send(:class_variable_set, :@@field_types, []) unless class_variable_defined?(:@@field_types)
31
+ end
32
+
33
+ # Accepts field definitions in the one of the follow formats:
34
+ #
35
+ # field :product
36
+ # field :product => Integer
37
+ #
38
+ # The order they're defined determines the order the will be output. The fields
39
+ # data is available by the standard accessors, class.product and class.product= etc...
40
+ # The value of the field will be cast to the type (if provided) when read from a file.
41
+ # The value is not touched when the type is not provided.
42
+ def self.field(args={})
43
+ # TODO: Examine casting from: http://codeforpeople.com/lib/ruby/fattr/fattr-1.0.3/
44
+ args = {args => nil} unless args.is_a? Hash
45
+
46
+ args.each_pair do |m,t|
47
+
48
+ [[:@@field_names, m], [:@@field_types, t]].each do |tuple|
49
+ class_variable_set(tuple[0], []) unless class_variable_defined?(tuple[0])
50
+ class_variable_set(tuple[0], class_variable_get(tuple[0]) << tuple[1])
51
+ end
52
+
53
+ next if method_defined?(m)
54
+
55
+ define_method(m) do instance_variable_get("@#{m}") end
56
+ define_method("#{m}=") do |val|
57
+ instance_variable_set("@#{m}",val)
58
+ end
59
+ end
60
+ end
61
+
62
+ # Returns an array of field names defined by self.field
63
+ def self.field_names
64
+ class_variable_get(:@@field_names)
65
+ end
66
+ # Ditto.
67
+ def field_names
68
+ self.class.send(:class_variable_get, :@@field_names)
69
+ end
70
+ # Returns an array of field types defined by self.field. Fields that did
71
+ # not receive a type are set to nil.
72
+ def self.field_types
73
+ class_variable_get(:@@field_types)
74
+ end
75
+ # Ditto.
76
+ def field_types
77
+ self.class.send(:class_variable_get, :@@field_types)
78
+ end
79
+
80
+ # Dump the object data to the given format.
81
+ def dump(format=nil, with_titles=true)
82
+ format ||= @format
83
+ raise "Format not defined (#{format})" unless SUPPORTED_FORMATS.member?(format)
84
+ send("to_#{format}", with_titles)
85
+ end
86
+
87
+ # Create a new instance of the object using data from file.
88
+ def self.from_file(file_path=nil, format=nil)
89
+ raise "Cannot read file (#{file_path})" unless File.exists?(file_path)
90
+ format = format || File.extname(file_path).tr('.', '')
91
+ me = send("from_#{format}", read_file_to_array(file_path))
92
+ me.format = format
93
+ me
94
+ end
95
+ # Write the object data to the given file.
96
+ def to_file(file_path=nil, with_titles=true)
97
+ raise "Cannot store to nil path" if file_path.nil?
98
+ format = File.extname(file_path).tr('.', '')
99
+ format ||= @format
100
+ Storable.write_file(file_path, dump(format, with_titles))
101
+ end
102
+
103
+ # Create a new instance of the object from a hash.
104
+ def self.from_hash(from={})
105
+ me = self.new
106
+
107
+ return me if !from || from.empty?
108
+
109
+ fnames = field_names
110
+ fnames.each_with_index do |key,index|
111
+
112
+
113
+
114
+ # TODO: Correct this horrible implementation (sorry, me. It's just one of those days.)
115
+
116
+ if field_types[index] == Time
117
+ value = Time.parse(from[key].to_s)
118
+ elsif field_types[index] == DateTime
119
+ value = DateTime.parse(from[key].to_s)
120
+ elsif field_types[index] == TrueClass
121
+ value = (from[key].to_s == "true")
122
+ elsif field_types[index] == Float
123
+ value = from[key].to_f
124
+ elsif field_types[index] == Integer
125
+ value = from[key].to_i
126
+ elsif field_types[index] == Array
127
+ (value ||= []) << from[key]
128
+ else
129
+ value = from[key] || from[key.to_s] # support for symbol keys and string keys
130
+ value = value.first if value.is_a?(Array) && value.size == 1 # I
131
+ end
132
+
133
+ me.send("#{key}=", value) if self.method_defined?("#{key}=")
134
+ end
135
+
136
+ me
137
+ end
138
+ # Return the object data as a hash
139
+ # +with_titles+ is ignored.
140
+ def to_hash(with_titles=true)
141
+ tmp = {}
142
+ field_names.each do |fname|
143
+ tmp[fname] = self.send(fname)
144
+ end
145
+ tmp
146
+ end
147
+
148
+ # Create a new instance of the object from YAML.
149
+ # +from+ a YAML string split into an array by line.
150
+ def self.from_yaml(from=[])
151
+ # from is an array of strings
152
+ from_str = from.join('')
153
+ hash = YAML::load(from_str)
154
+ hash = from_hash(hash) if hash.is_a? Hash
155
+ hash
156
+ end
157
+ def to_yaml(with_titles=true)
158
+ to_hash.to_yaml
159
+ end
160
+
161
+ # Create a new instance of the object from a JSON string.
162
+ # +from+ a JSON string split into an array by line.
163
+ def self.from_json(from=[])
164
+ require 'json'
165
+ # from is an array of strings
166
+ from_str = from.join('')
167
+ tmp = JSON::load(from_str)
168
+ hash_sym = tmp.keys.inject({}) do |hash, key|
169
+ hash[key.to_sym] = tmp[key]
170
+ hash
171
+ end
172
+ hash_sym = from_hash(hash_sym) if hash_sym.is_a? Hash
173
+ hash_sym
174
+ end
175
+ def to_json(with_titles=true)
176
+ require 'json'
177
+ to_hash.to_json
178
+ end
179
+
180
+ # Return the object data as a delimited string.
181
+ # +with_titles+ specifiy whether to include field names (default: false)
182
+ # +delim+ is the field delimiter.
183
+ def to_delimited(with_titles=false, delim=',')
184
+ values = []
185
+ field_names.each do |fname|
186
+ values << self.send(fname.to_s) # TODO: escape values
187
+ end
188
+ output = values.join(delim)
189
+ output = field_names.join(delim) << $/ << output if with_titles
190
+ output
191
+ end
192
+ # Return the object data as a tab delimited string.
193
+ # +with_titles+ specifiy whether to include field names (default: false)
194
+ def to_tsv(with_titles=false)
195
+ to_delimited(with_titles, "\t")
196
+ end
197
+ # Return the object data as a comma delimited string.
198
+ # +with_titles+ specifiy whether to include field names (default: false)
199
+ def to_csv(with_titles=false)
200
+ to_delimited(with_titles, ',')
201
+ end
202
+ # Create a new instance from tab-delimited data.
203
+ # +from+ a JSON string split into an array by line.
204
+ def self.from_tsv(from=[])
205
+ self.from_delimited(from, "\t")
206
+ end
207
+ # Create a new instance of the object from comma-delimited data.
208
+ # +from+ a JSON string split into an array by line.
209
+ def self.from_csv(from=[])
210
+ self.from_delimited(from, ',')
211
+ end
212
+
213
+ # Create a new instance of the object from a delimited string.
214
+ # +from+ a JSON string split into an array by line.
215
+ # +delim+ is the field delimiter.
216
+ def self.from_delimited(from=[],delim=',')
217
+ return if from.empty?
218
+ # We grab an instance of the class so we can
219
+ hash = {}
220
+
221
+ fnames = values = []
222
+ if (from.size > 1 && !from[1].empty?)
223
+ fnames = from[0].chomp.split(delim)
224
+ values = from[1].chomp.split(delim)
225
+ else
226
+ fnames = self.field_names
227
+ values = from[0].chomp.split(delim)
228
+ end
229
+
230
+ fnames.each_with_index do |key,index|
231
+ next unless values[index]
232
+ hash[key.to_sym] = values[index]
233
+ end
234
+ hash = from_hash(hash) if hash.is_a? Hash
235
+ hash
236
+ end
237
+
238
+ def self.read_file_to_array(path)
239
+ contents = []
240
+ return contents unless File.exists?(path)
241
+
242
+ open(path, 'r') do |l|
243
+ contents = l.readlines
244
+ end
245
+
246
+ contents
247
+ end
248
+
249
+ def self.write_file(path, content, flush=true)
250
+ write_or_append_file('w', path, content, flush)
251
+ end
252
+
253
+ def self.append_file(path, content, flush=true)
254
+ write_or_append_file('a', path, content, flush)
255
+ end
256
+
257
+ def self.write_or_append_file(write_or_append, path, content = '', flush = true)
258
+ #STDERR.puts "Writing to #{ path }..."
259
+ create_dir(File.dirname(path))
260
+
261
+ open(path, write_or_append) do |f|
262
+ f.puts content
263
+ f.flush if flush;
264
+ end
265
+ File.chmod(0600, path)
266
+ end
267
+ end
268
+
@@ -0,0 +1,52 @@
1
+ @spec = Gem::Specification.new do |s|
2
+ s.name = "rudy"
3
+ s.version = "0.2"
4
+ s.summary = "Rudy is a handy staging and deployment tool for Amazon EC2."
5
+ s.description = "Rudy is a handy staging and deployment tool for Amazon EC2."
6
+ s.author = "Delano Mandelbaum"
7
+ s.email = "delano@solutious.com"
8
+ s.homepage = "http://github.com/solutious/rudy"
9
+
10
+ # = MANIFEST =
11
+ # find {bin,lib,support,tryouts} -type f | grep -v git
12
+ s.files = %w(
13
+ CHANGES.txt
14
+ LICENSE.txt
15
+ README.rdoc
16
+ Rakefile
17
+ bin/rudy
18
+ lib/drydock.rb
19
+ lib/rudy.rb
20
+ lib/rudy/aws.rb
21
+ lib/rudy/aws/ec2.rb
22
+ lib/rudy/aws/s3.rb
23
+ lib/rudy/aws/simpledb.rb
24
+ lib/rudy/command/addresses.rb
25
+ lib/rudy/command/base.rb
26
+ lib/rudy/command/commit.rb
27
+ lib/rudy/command/disks.rb
28
+ lib/rudy/command/environment.rb
29
+ lib/rudy/command/groups.rb
30
+ lib/rudy/command/images.rb
31
+ lib/rudy/command/instances.rb
32
+ lib/rudy/command/metadata.rb
33
+ lib/rudy/command/release.rb
34
+ lib/rudy/command/volumes.rb
35
+ lib/rudy/metadata/disk.rb
36
+ lib/rudy/metadata/environment.rb
37
+ lib/rudy/scm/svn.rb
38
+ lib/rudy/utils.rb
39
+ lib/storable.rb
40
+ rudy.gemspec
41
+ support/rudy-ec2-startup
42
+ )
43
+ s.executables = %w[rudy]
44
+
45
+ s.extra_rdoc_files = %w[README.rdoc LICENSE.txt]
46
+ s.has_rdoc = true
47
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Rudy: Your friend in staging and deploying with EC2", "--main", "README.rdoc"]
48
+ s.require_paths = %w[lib]
49
+ s.rubygems_version = '1.1.1'
50
+
51
+ s.rubyforge_project = 'rudy'
52
+ end
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # what: Rudy EC2 startup script
4
+ # who: delano@solutious.com
5
+ # when: 2009-02-20 (rev: 3)
6
+
7
+ # NOTE: This is a prototype version of this script. A cleaner version
8
+ # with better documentation is forthcoming.
9
+
10
+ # Runs when an ec2 instance startups up.
11
+ # Grabs configuration from the run time user data and stores it locally.
12
+ # Expects message data in the yaml format:
13
+ #
14
+ # :dbmaster: IP ADDRESS or LOCAL HOSTNAME (default: localhost)
15
+ # :role: app, db, etc... (default: app)
16
+ # :env: stage, prod (default: stage)
17
+ # :access_key: amazon access key
18
+ # :secret_key: amazon secret key
19
+
20
+ # Put this in /etc/init.d. Then:
21
+ # * chmod 755 rudy-ec2-startup
22
+ # * cd /etc/rc3.d
23
+ # * ln -s ../init.d/rudy-ec2-startup S17rudy
24
+ # * cd /etc/rc4.d
25
+ # * ln -s ../init.d/rudy-ec2-startup S17rudy
26
+ # * cd /etc/rc5.d
27
+ # * ln -s ../init.d/rudy-ec2-startup S17rudy
28
+
29
+ require 'yaml'
30
+ require 'resolv'
31
+
32
+ LOGFILE = '/var/log/rudy-ec2-startup'
33
+ USERDATA = 'http://169.254.169.254/2008-02-01/user-data'
34
+ METADATA = 'http://169.254.169.254/2008-02-01/meta-data'
35
+ METADATAPARAMS = ['instance-id', 'instance-type']
36
+
37
+
38
+ def get_metadata
39
+ metadata = {}
40
+
41
+ begin
42
+ METADATAPARAMS.each do |param|
43
+ log " ---> #{param}"
44
+ metadata[param.to_s] = run("curl -s #{METADATA}/#{param}")
45
+ end
46
+ rescue => ex
47
+ log("Problem getting metadata: #{ex.message}")
48
+ end
49
+ metadata
50
+ end
51
+
52
+ def run(command, input='')
53
+ IO.popen(command, 'r+') do |io|
54
+ #io.puts input
55
+ #io.close_write
56
+ return io.read
57
+ end
58
+ end
59
+
60
+ def get_formatted_time
61
+ t = Time.now
62
+ t_out = t.strftime("%H:%M:%S%p (%m/%d/%Y)")
63
+ end
64
+
65
+ def write_to_file (filename, s, type='a')
66
+ f = File.open(filename,type)
67
+ f.puts s
68
+ f.close
69
+ end
70
+
71
+ def read_file(path)
72
+ read_file_to_array(path).join('')
73
+ end
74
+
75
+ def read_file_to_array(path)
76
+ contents = []
77
+ return contents unless File.exists?(path)
78
+ open(path, 'r') do |l|
79
+ contents = l.readlines
80
+ end
81
+ contents
82
+ end
83
+
84
+ def log(s)
85
+ msg = "#{get_formatted_time}: #{s}"
86
+ write_to_file(LOGFILE, msg)
87
+ puts msg
88
+ end
89
+
90
+ begin
91
+ File.unlink(LOGFILE) if File.exists?(LOGFILE)
92
+ log "Deleted previous logfile (#{LOGFILE})"
93
+ log "Grabing configuration..."
94
+ config_yaml = run("curl -s #{USERDATA}")
95
+
96
+ log "Grabing meta-data..."
97
+ metadata = get_metadata
98
+
99
+ raise "Ooooooops, no configuration" if !config_yaml || config_yaml.empty?
100
+
101
+ config = YAML::load(config_yaml)
102
+
103
+ if config && config[:role]
104
+ myrole = config[:role].downcase
105
+
106
+ else
107
+ myrole = "app"
108
+ end
109
+
110
+ log "My role is: #{myrole}"
111
+
112
+ env = config && config[:env] ? config[:env] : "stage"
113
+
114
+ unless read_file('/etc/hosts') =~ /dbmaster/
115
+ if config && config[:dbmaster]
116
+ log "Updating dbmaster in /etc/hosts..."
117
+
118
+ case config[:dbmaster]
119
+ when /^\d.+/
120
+ ip_address = config[:dbmaster]
121
+ else
122
+ ip_address = Resolv.getaddress(config[:dbmaster])
123
+ end
124
+
125
+ log "The IP address for my dbmaster is: #{ip_address}"
126
+ write_to_file("/etc/hosts", "\n#{ip_address}\tdbmaster\n")
127
+
128
+ else
129
+ write_to_file("/etc/hosts", "\n127.0.0.1\tdbmaster\n")
130
+ end
131
+ end
132
+
133
+
134
+ hostname = ""
135
+ myid = "default-"
136
+ if metadata['instance-id']
137
+ myid = metadata['instance-id'][2..metadata['instance-id'].length]
138
+ log "My ID: #{myid}"
139
+ write_to_file("/etc/instance-id", myid, "w")
140
+ end
141
+
142
+ mytype = "unknown"
143
+ if metadata['instance-type']
144
+ mytype = metadata['instance-type']
145
+ log "My instance type: #{metadata['instance-type']}"
146
+ write_to_file("/etc/instance-type", metadata['instance-type'], "w")
147
+ end
148
+
149
+
150
+ hostname = "#{env}-#{myrole}-#{mytype.gsub('.', '')}-#{myid}"
151
+ log "Setting hostname to #{hostname}"
152
+ `hostname #{hostname}`
153
+
154
+ unless read_file('/etc/hosts') =~ /#{hostname}/
155
+ log "Adding an entry to /etc/hosts for: #{hostname}"
156
+ write_to_file("/etc/hosts", "\n127.0.0.1\t#{hostname}")
157
+ end
158
+
159
+ log "Done!"
160
+
161
+ rescue => ex
162
+ log ex.message
163
+ end
164
+
165
+
166
+