sunshine 1.0.0.pre

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