sunshine 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/History.txt +237 -0
  2. data/Manifest.txt +70 -0
  3. data/README.txt +277 -0
  4. data/Rakefile +46 -0
  5. data/bin/sunshine +5 -0
  6. data/examples/deploy.rb +61 -0
  7. data/examples/deploy_tasks.rake +112 -0
  8. data/examples/standalone_deploy.rb +31 -0
  9. data/lib/commands/add.rb +96 -0
  10. data/lib/commands/default.rb +169 -0
  11. data/lib/commands/list.rb +322 -0
  12. data/lib/commands/restart.rb +62 -0
  13. data/lib/commands/rm.rb +83 -0
  14. data/lib/commands/run.rb +151 -0
  15. data/lib/commands/start.rb +72 -0
  16. data/lib/commands/stop.rb +61 -0
  17. data/lib/sunshine/app.rb +876 -0
  18. data/lib/sunshine/binder.rb +70 -0
  19. data/lib/sunshine/crontab.rb +143 -0
  20. data/lib/sunshine/daemon.rb +380 -0
  21. data/lib/sunshine/daemons/ar_sendmail.rb +28 -0
  22. data/lib/sunshine/daemons/delayed_job.rb +30 -0
  23. data/lib/sunshine/daemons/nginx.rb +104 -0
  24. data/lib/sunshine/daemons/rainbows.rb +35 -0
  25. data/lib/sunshine/daemons/server.rb +66 -0
  26. data/lib/sunshine/daemons/unicorn.rb +26 -0
  27. data/lib/sunshine/dependencies.rb +103 -0
  28. data/lib/sunshine/dependency_lib.rb +200 -0
  29. data/lib/sunshine/exceptions.rb +54 -0
  30. data/lib/sunshine/healthcheck.rb +83 -0
  31. data/lib/sunshine/output.rb +131 -0
  32. data/lib/sunshine/package_managers/apt.rb +48 -0
  33. data/lib/sunshine/package_managers/dependency.rb +349 -0
  34. data/lib/sunshine/package_managers/gem.rb +54 -0
  35. data/lib/sunshine/package_managers/yum.rb +62 -0
  36. data/lib/sunshine/remote_shell.rb +241 -0
  37. data/lib/sunshine/repo.rb +128 -0
  38. data/lib/sunshine/repos/git_repo.rb +122 -0
  39. data/lib/sunshine/repos/rsync_repo.rb +29 -0
  40. data/lib/sunshine/repos/svn_repo.rb +78 -0
  41. data/lib/sunshine/server_app.rb +554 -0
  42. data/lib/sunshine/shell.rb +384 -0
  43. data/lib/sunshine.rb +391 -0
  44. data/templates/logrotate/logrotate.conf.erb +11 -0
  45. data/templates/nginx/nginx.conf.erb +109 -0
  46. data/templates/nginx/nginx_optimize.conf +23 -0
  47. data/templates/nginx/nginx_proxy.conf +13 -0
  48. data/templates/rainbows/rainbows.conf.erb +18 -0
  49. data/templates/tasks/sunshine.rake +114 -0
  50. data/templates/unicorn/unicorn.conf.erb +6 -0
  51. data/test/fixtures/app_configs/test_app.yml +11 -0
  52. data/test/fixtures/sunshine_test/test_upload +0 -0
  53. data/test/mocks/mock_object.rb +179 -0
  54. data/test/mocks/mock_open4.rb +117 -0
  55. data/test/test_helper.rb +188 -0
  56. data/test/unit/test_app.rb +489 -0
  57. data/test/unit/test_binder.rb +20 -0
  58. data/test/unit/test_crontab.rb +128 -0
  59. data/test/unit/test_git_repo.rb +26 -0
  60. data/test/unit/test_healthcheck.rb +70 -0
  61. data/test/unit/test_nginx.rb +107 -0
  62. data/test/unit/test_rainbows.rb +26 -0
  63. data/test/unit/test_remote_shell.rb +102 -0
  64. data/test/unit/test_repo.rb +42 -0
  65. data/test/unit/test_server.rb +324 -0
  66. data/test/unit/test_server_app.rb +425 -0
  67. data/test/unit/test_shell.rb +97 -0
  68. data/test/unit/test_sunshine.rb +157 -0
  69. data/test/unit/test_svn_repo.rb +55 -0
  70. data/test/unit/test_unicorn.rb +22 -0
  71. metadata +217 -0
@@ -0,0 +1,70 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Instantiated per deploy server and used to pass bindings to the config's
5
+ # ERB build method:
6
+ # binder.set :server_name, "blah.com"
7
+ # binder.forward :server_method, ...
8
+ # binder.get_binding
9
+
10
+ class Binder
11
+
12
+ def initialize target
13
+ @target = target
14
+ end
15
+
16
+
17
+ ##
18
+ # Set the binding instance variable and accessor method.
19
+
20
+ def set key, value=nil, &block
21
+ value ||= block if block_given?
22
+
23
+ instance_variable_set("@#{key}", value)
24
+
25
+ eval_str = <<-STR
26
+ undef #{key} if defined?(#{key})
27
+ def #{key}(*args)
28
+ if Proc === @#{key}
29
+ @#{key}.call(*args)
30
+ else
31
+ @#{key}
32
+ end
33
+ end
34
+ STR
35
+
36
+ instance_eval eval_str, __FILE__, __LINE__ + 1
37
+ end
38
+
39
+
40
+ ##
41
+ # Takes a hash and assign each hash key/value as an attribute.
42
+
43
+ def import_hash hash
44
+ hash.each{|k, v| self.set(k, v)}
45
+ end
46
+
47
+
48
+ ##
49
+ # Forward a method to the server instance.
50
+
51
+ def forward *method_names
52
+ method_names.each do |method_name|
53
+ instance_eval <<-STR, __FILE__, __LINE__ + 1
54
+ undef #{method_name} if defined?(#{method_name})
55
+ def #{method_name}(*args, &block)
56
+ @target.#{method_name}(*args, &block)
57
+ end
58
+ STR
59
+ end
60
+ end
61
+
62
+
63
+ ##
64
+ # Retrieve the object's binding.
65
+
66
+ def get_binding
67
+ binding
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,143 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # A simple namespaced grouping of cron jobs that can be written
5
+ # to a deploy server.
6
+
7
+ class Crontab
8
+
9
+ attr_reader :name, :shell
10
+
11
+ def initialize name, shell
12
+ @name = name
13
+ @shell = shell
14
+ @jobs = nil
15
+ end
16
+
17
+
18
+ ##
19
+ # Get the jobs matching this crontab. Loads them from the crontab
20
+ # if @jobs hasn't been set yet.
21
+
22
+ def jobs
23
+ @jobs ||= parse read_crontab
24
+ end
25
+
26
+
27
+ ##
28
+ # Build the crontab by replacing preexisting cron jobs and adding new ones.
29
+
30
+ def build crontab=""
31
+ crontab.strip!
32
+
33
+ jobs.each do |namespace, cron_job|
34
+ crontab = delete_jobs crontab, namespace
35
+
36
+ start_id, end_id = get_job_ids namespace
37
+ cron_str = "\n#{start_id}\n#{cron_job.chomp}\n#{end_id}\n\n"
38
+
39
+ crontab << cron_str
40
+ end
41
+
42
+ crontab
43
+ end
44
+
45
+
46
+ ##
47
+ # Remove all cron jobs that reference crontab.name.
48
+
49
+ def delete!
50
+ crontab = read_crontab
51
+ crontab = delete_jobs crontab
52
+
53
+ write_crontab crontab
54
+
55
+ crontab
56
+ end
57
+
58
+
59
+ ##
60
+ # Write the crontab on the given shell
61
+
62
+ def write!
63
+ return unless modified?
64
+
65
+ crontab = read_crontab
66
+ crontab = delete_jobs crontab
67
+ crontab = build crontab
68
+
69
+ write_crontab crontab
70
+
71
+ crontab
72
+ end
73
+
74
+
75
+ ##
76
+ # Checks if the crontab was modified for crontab.name.
77
+
78
+ def modified?
79
+ !@jobs.nil?
80
+ end
81
+
82
+
83
+ ##
84
+ # Load a crontab string and parse out jobs related to crontab.name.
85
+ # Returns a hash of namespace/jobs pairs.
86
+
87
+ def parse string
88
+ jobs = Hash.new
89
+
90
+ namespace = nil
91
+
92
+ string.each_line do |line|
93
+ if line =~ /^# sunshine #{@name}:(.*):begin/
94
+ namespace = $1
95
+ next
96
+ elsif line =~ /^# sunshine #{@name}:#{namespace}:end/
97
+ namespace = nil
98
+ end
99
+
100
+ if namespace
101
+ jobs[namespace] ||= String.new
102
+ jobs[namespace] << line
103
+ end
104
+ end
105
+
106
+ jobs
107
+ end
108
+
109
+
110
+ ##
111
+ # Returns the shell's crontab as a string
112
+
113
+ def read_crontab
114
+ @shell.call("crontab -l") rescue ""
115
+ end
116
+
117
+
118
+ private
119
+
120
+ def write_crontab content
121
+ @shell.call("echo '#{content.gsub(/'/){|s| "'\\''"}}' | crontab")
122
+ end
123
+
124
+
125
+ def delete_jobs crontab, namespace=nil
126
+ start_id, end_id = get_job_ids namespace
127
+
128
+ crontab.gsub!(/^#{start_id}$(.*?)^#{end_id}$\n*/m, "")
129
+
130
+ crontab
131
+ end
132
+
133
+
134
+ def get_job_ids namespace=nil
135
+ namespace ||= "[^\n]*"
136
+
137
+ start_id = "# sunshine #{@name}:#{namespace}:begin"
138
+ end_id = "# sunshine #{@name}:#{namespace}:end"
139
+
140
+ return start_id, end_id
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,380 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # An abstract class to wrap simple daemon software setup and start/stop.
5
+ #
6
+ # Child classes are expected to at least provide a start and stop bash script
7
+ # by either overloading the start_cmd and stop_cmd methods, or by setting
8
+ # @start_cmd and @stop_cmd. A restart_cmd method or @restart_cmd attribute
9
+ # may also be specified if restart requires more functionality than simply
10
+ # calling start_cmd && stop_cmd.
11
+
12
+ class Daemon
13
+
14
+
15
+ ##
16
+ # Returns an array of method names to assign to the binder
17
+ # for template rendering.
18
+
19
+ def self.binder_methods
20
+ [:app, :name, :target, :bin, :pid, :port,
21
+ :processes, :config_path, :log_file, :timeout]
22
+ end
23
+
24
+
25
+ ##
26
+ # Turn camelcase into underscore. Used for Daemon#name.
27
+
28
+ def self.underscore str
29
+ str.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
30
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
31
+ end
32
+
33
+
34
+ attr_reader :app, :name, :target
35
+
36
+ attr_accessor :bin, :pid, :processes, :timeout, :sudo, :server_apps
37
+
38
+ attr_accessor :config_template, :config_path, :config_file
39
+
40
+ attr_writer :start_cmd, :stop_cmd, :restart_cmd, :status_cmd
41
+
42
+
43
+ # Daemon objects need only an App object to be instantiated but many options
44
+ # are available for customization:
45
+ #
46
+ # :pid:: pid_path - set the pid; default: app.shared_path/pids/svr_name.pid
47
+ # defaults to app.shared_path/pids/svr_name.pid
48
+ #
49
+ # :bin:: bin_path - set the daemon app bin path (e.g. usr/local/nginx)
50
+ # defaults to svr_name
51
+ #
52
+ # :sudo:: bool|str - define if sudo should be used and with what user
53
+ #
54
+ # :timeout:: int|str - timeout to use for daemon config
55
+ # defaults to 1
56
+ #
57
+ # :processes:: prcss_num - number of processes daemon should run
58
+ # defaults to 0
59
+ #
60
+ # :config_template:: path - glob path to tempates to render and upload
61
+ # defaults to sunshine_path/templates/svr_name/*
62
+ #
63
+ # :config_path:: path - remote path daemon configs will be uploaded to
64
+ # defaults to app.current_path/daemons/svr_name
65
+ #
66
+ # :config_file:: name - remote file name the daemon should load
67
+ # defaults to svr_name.conf
68
+ #
69
+ # :log_path:: path - path to where the log files should be output
70
+ # defaults to app.log_path
71
+ #
72
+ # :point_to:: app|daemon - an abstract target to point to
73
+ # defaults to the passed app
74
+ #
75
+ # The Daemon constructor also supports any App#find options to narrow
76
+ # the server apps to use. Note: subclasses such as Server already have
77
+ # a default :role that can be overridden.
78
+
79
+ def initialize app, options={}
80
+ @options = options
81
+
82
+ @app = app
83
+ @target = options[:point_to] || @app
84
+
85
+ @short_class_name = self.class.underscore self.class.to_s.split("::").last
86
+
87
+ @name = options[:name] || @short_class_name
88
+
89
+ @pid = options[:pid] || "#{@app.shared_path}/pids/#{@name}.pid"
90
+ @bin = options[:bin] || @name
91
+ @sudo = options[:sudo]
92
+ @timeout = options[:timeout] || 0
93
+ @dep_name = options[:dep_name] || @name
94
+ @processes = options[:processes] || 1
95
+
96
+ @config_template = options[:config_template]
97
+ @config_template ||= "#{Sunshine::ROOT}/templates/#{@short_class_name}/*"
98
+
99
+ @config_path = options[:config_path] ||
100
+ "#{@app.current_path}/daemons/#{@name}"
101
+ @config_file = options[:config_file] || "#{@name}.conf"
102
+
103
+ log_path = options[:log_path] || @app.log_path
104
+ @log_files = {
105
+ :stderr => "#{log_path}/#{@name}_stderr.log",
106
+ :stdout => "#{log_path}/#{@name}_stdout.log"
107
+ }
108
+
109
+ @start_cmd = @stop_cmd = @restart_cmd = @status_cmd = nil
110
+
111
+ @setup_successful = nil
112
+
113
+ register_after_user_script
114
+ end
115
+
116
+
117
+ ##
118
+ # Do something with each server app used by the daemon.
119
+
120
+ def each_server_app(&block)
121
+ @app.each(@options, &block)
122
+ end
123
+
124
+
125
+ ##
126
+ # Setup the daemon, parse and upload config templates.
127
+ # If a dependency with the daemon name exists in Sunshine.dependencies,
128
+ # setup will attempt to install the dependency before uploading configs.
129
+ # If a block is given it will be passed each server_app and binder object
130
+ # which will be used for the building erb config templates.
131
+ # See the ConfigBinding class for more information.
132
+
133
+ def setup
134
+ Sunshine.logger.info @name, "Setting up #{@name} daemon" do
135
+
136
+ each_server_app do |server_app|
137
+
138
+ # Build erb binding
139
+ binder = config_binding server_app.shell
140
+
141
+ server_app.shell.call "mkdir -p #{remote_dirs.join(" ")}",
142
+ :sudo => binder.sudo
143
+
144
+ yield(server_app, binder) if block_given?
145
+
146
+ server_app.install_deps @dep_name if
147
+ Sunshine.dependencies.exist?(@dep_name)
148
+
149
+ upload_config_files(server_app.shell, binder.get_binding)
150
+ end
151
+ end
152
+
153
+ @setup_successful = true
154
+
155
+ rescue => e
156
+ raise CriticalDeployError.new(e, "Could not setup #{@name}")
157
+ end
158
+
159
+
160
+ ##
161
+ # Check if setup was run successfully.
162
+
163
+ def has_setup? force=false
164
+ return @setup_successful unless @setup_successful.nil? || force
165
+
166
+ each_server_app do |server_app|
167
+
168
+ unless server_app.shell.file? config_file_path
169
+ return @setup_successful = false
170
+ end
171
+ end
172
+
173
+ @setup_successful = true
174
+ end
175
+
176
+
177
+ ##
178
+ # Start the daemon app after running setup.
179
+
180
+ def start
181
+ self.setup unless has_setup?
182
+ Sunshine.logger.info @name, "Starting #{@name} daemon" do
183
+
184
+ each_server_app do |server_app|
185
+ begin
186
+ server_app.shell.call start_cmd,
187
+ :sudo => pick_sudo(server_app.shell)
188
+
189
+ yield(server_app) if block_given?
190
+ rescue => e
191
+ raise CriticalDeployError.new(e, "Could not start #{@name}")
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+
198
+ ##
199
+ # Stop the daemon app.
200
+
201
+ def stop
202
+ Sunshine.logger.info @name, "Stopping #{@name} daemon" do
203
+
204
+ each_server_app do |server_app|
205
+ begin
206
+ server_app.shell.call stop_cmd,
207
+ :sudo => pick_sudo(server_app.shell)
208
+
209
+ yield(server_app) if block_given?
210
+ rescue => e
211
+ raise CriticalDeployError.new(e, "Could not stop #{@name}")
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+
218
+ ##
219
+ # Restarts the daemon using the restart_cmd attribute if provided.
220
+ # If restart_cmd is not provided, calls stop and start.
221
+
222
+ def restart
223
+ self.setup unless has_setup?
224
+
225
+ Sunshine.logger.info @name, "Restarting #{@name} daemon" do
226
+ each_server_app do |server_app|
227
+ begin
228
+ server_app.shell.call restart_cmd,
229
+ :sudo => pick_sudo(server_app.shell)
230
+
231
+ yield(server_app) if block_given?
232
+ rescue => e
233
+ raise CriticalDeployError.new(e, "Could not restart #{@name}")
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+
240
+ ##
241
+ # Gets the command that starts the daemon.
242
+ # Should be overridden by child classes.
243
+
244
+ def start_cmd
245
+ return @start_cmd ||
246
+ raise(CriticalDeployError, "@start_cmd undefined. Can't start #{@name}")
247
+ end
248
+
249
+
250
+ ##
251
+ # Gets the command that stops the daemon.
252
+ # Should be overridden by child classes.
253
+
254
+ def stop_cmd
255
+ return @stop_cmd ||
256
+ raise(CriticalDeployError, "@stop_cmd undefined. Can't stop #{@name}")
257
+ end
258
+
259
+
260
+ ##
261
+ # Gets the command that restarts the daemon.
262
+
263
+ def restart_cmd
264
+ @restart_cmd || [stop_cmd, start_cmd].map{|c| "(#{c})"}.join(" && ")
265
+ end
266
+
267
+
268
+ ##
269
+ # Get the command to check if the daemon is running.
270
+
271
+ def status_cmd
272
+ @status_cmd || "test -f #{@pid}"
273
+ end
274
+
275
+
276
+ ##
277
+ # Append or override daemon log file paths:
278
+ # daemon.log_files :stderr => "/all_logs/stderr.log"
279
+
280
+ def log_files(hash)
281
+ @log_files.merge!(hash)
282
+ end
283
+
284
+
285
+ ##
286
+ # Get the path of a log file:
287
+ # daemon.log_file(:stderr)
288
+ # #=> "/all_logs/stderr.log"
289
+
290
+ def log_file(key)
291
+ @log_files[key]
292
+ end
293
+
294
+
295
+ ##
296
+ # Get the file path to the daemon's config file.
297
+
298
+ def config_file_path
299
+ "#{@config_path}/#{@config_file}"
300
+ end
301
+
302
+
303
+ ##
304
+ # Upload config files and run them through erb with the provided
305
+ # binding if necessary.
306
+
307
+ def upload_config_files(shell, setup_binding=binding)
308
+ config_template_files.each do |config_file|
309
+
310
+ if File.extname(config_file) == ".erb"
311
+ filename = File.basename(config_file[0..-5])
312
+ parsed_config = @app.build_erb(config_file, setup_binding)
313
+ shell.make_file "#{@config_path}/#{filename}", parsed_config
314
+ else
315
+ filename = File.basename(config_file)
316
+ shell.upload config_file, "#{@config_path}/#{filename}"
317
+ end
318
+ end
319
+ end
320
+
321
+
322
+ ##
323
+ # Get the array of local config template files needed by the daemon.
324
+
325
+ def config_template_files
326
+ @config_template_files ||= Dir[@config_template].select{|f| File.file?(f)}
327
+ end
328
+
329
+
330
+ private
331
+
332
+ def config_binding shell
333
+ binder = Binder.new self
334
+ binder.forward(*self.class.binder_methods)
335
+
336
+ binder.set :shell, shell
337
+
338
+ binder_sudo = pick_sudo(shell)
339
+ binder.set :sudo, binder_sudo
340
+
341
+ binder.set :expand_path do |path|
342
+ shell.expand_path path
343
+ end
344
+
345
+ binder
346
+ end
347
+
348
+
349
+ def pick_sudo shell
350
+ case shell.sudo
351
+ when true
352
+ self.sudo || shell.sudo
353
+ when String
354
+ String === self.sudo ? self.sudo : shell.sudo
355
+ else
356
+ self.sudo
357
+ end
358
+ end
359
+
360
+
361
+ def remote_dirs
362
+ dirs = @log_files.values.map{|f| File.dirname(f)}
363
+ dirs.concat [@config_path, File.dirname(@pid)]
364
+ dirs.delete_if{|d| d == "."}
365
+ dirs
366
+ end
367
+
368
+
369
+ def register_after_user_script
370
+ @app.after_user_script do |app|
371
+ each_server_app do |sa|
372
+ sa.scripts[:start] << start_cmd
373
+ sa.scripts[:stop] << stop_cmd
374
+ sa.scripts[:restart] << restart_cmd
375
+ sa.scripts[:status] << status_cmd
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,28 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Simple daemon wrapper for ar_sendmail setup and control.
5
+ # By default, uses server apps with the :mail role.
6
+
7
+ class ARSendmail < Daemon
8
+
9
+ def initialize app, options={}
10
+ options[:role] ||= :mail
11
+
12
+ super app, options
13
+
14
+ @dep_name = options[:dep_name] || 'ar_mailer'
15
+ end
16
+
17
+
18
+ def start_cmd
19
+ "cd #{@app.current_path} && #{@bin} -p #{@pid} -d"
20
+ end
21
+
22
+
23
+ def stop_cmd
24
+ "test -f #{@pid} && kill `cat #{@pid}` || "+
25
+ "echo 'No #{@name} process to stop for #{@app.name}'; rm -f #{@pid};"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Simple daemon wrapper for delayed_job daemon setup and control.
5
+ # By default, uses server apps with the :dj role. Supports
6
+ # the :processes option.
7
+
8
+ class DelayedJob < Daemon
9
+
10
+ def initialize app, options={}
11
+ options[:role] ||= :dj
12
+
13
+ super app, options
14
+
15
+ @pid = "#{@app.current_path}/tmp/pids/delayed_job.pid"
16
+
17
+ @dep_name = options[:dep_name] || "daemons"
18
+ end
19
+
20
+
21
+ def start_cmd
22
+ "cd #{@app.current_path} && script/delayed_job -n #{@processes} start"
23
+ end
24
+
25
+
26
+ def stop_cmd
27
+ "cd #{@app.current_path} && script/delayed_job stop"
28
+ end
29
+ end
30
+ end