rudy 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+