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,384 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # The Shell class handles local input, output and execution to the shell.
5
+
6
+ class Shell
7
+
8
+ include Open4
9
+
10
+ class TimeoutError < FatalDeployError; end
11
+
12
+ ##
13
+ # Time to wait with no activity until giving up on a command.
14
+ TIMEOUT = 120
15
+
16
+ LOCAL_USER = `whoami`.chomp
17
+ LOCAL_HOST = `hostname`.chomp
18
+
19
+ SUDO_FAILED = /^Sorry, try again./
20
+ SUDO_PROMPT = /^Password:/
21
+
22
+ attr_reader :user, :host, :password, :input, :output, :mutex
23
+ attr_accessor :env, :sudo
24
+
25
+ def initialize output = $stdout, options={}
26
+ @output = output
27
+
28
+ $stdin.sync
29
+ @input = HighLine.new $stdin
30
+
31
+ @user = LOCAL_USER
32
+ @host = LOCAL_HOST
33
+
34
+ @sudo = options[:sudo]
35
+ @env = options[:env] || {}
36
+ @password = options[:password]
37
+
38
+ @cmd_activity = nil
39
+
40
+ @mutex = nil
41
+ end
42
+
43
+
44
+ ##
45
+ # Checks for equality
46
+
47
+ def == shell
48
+ @host == shell.host && @user == shell.user rescue false
49
+ end
50
+
51
+
52
+ ##
53
+ # Prompt the user for input.
54
+
55
+ def ask(*args, &block)
56
+ sync{ @input.ask(*args, &block) }
57
+ end
58
+
59
+
60
+ ##
61
+ # Prompt the user to agree.
62
+
63
+ def agree(*args, &block)
64
+ sync{ @input.agree(*args, &block) }
65
+ end
66
+
67
+
68
+ ##
69
+ # Execute a command on the local system and return the output.
70
+
71
+ def call cmd, options={}, &block
72
+ execute sudo_cmd(cmd, options), &block
73
+ end
74
+
75
+
76
+ ##
77
+ # Close the output IO. (Required by the Logger class)
78
+
79
+ def close
80
+ @output.close
81
+ end
82
+
83
+
84
+ ##
85
+ # Returns true. Compatibility method with RemoteShell.
86
+
87
+ def connect
88
+ true
89
+ end
90
+
91
+
92
+ ##
93
+ # Returns true. Compatibility method with RemoteShell.
94
+
95
+ def connected?
96
+ true
97
+ end
98
+
99
+
100
+ ##
101
+ # Returns true. Compatibility method with RemoteShell.
102
+
103
+ def disconnect
104
+ true
105
+ end
106
+
107
+
108
+ ##
109
+ # Copies a file. Compatibility method with RemoteShell.
110
+
111
+ def download from_path, to_path, options={}, &block
112
+ Sunshine.logger.info @host, "Copying #{from_path} -> #{to_path}" do
113
+ FileUtils.cp_r from_path, to_path
114
+ end
115
+ end
116
+
117
+ alias upload download
118
+
119
+
120
+ ##
121
+ # Expands the path. Compatibility method with RemoteShell.
122
+
123
+ def expand_path path
124
+ File.expand_path path
125
+ end
126
+
127
+
128
+ ##
129
+ # Checks if file exists. Compatibility method with RemoteShell.
130
+
131
+ def file? filepath
132
+ File.file? filepath
133
+ end
134
+
135
+
136
+ ##
137
+ # Write a file. Compatibility method with RemoteShell.
138
+
139
+ def make_file filepath, content, options={}
140
+ File.open(filepath, "w+"){|f| f.write(content)}
141
+ end
142
+
143
+
144
+ ##
145
+ # Get the name of the OS
146
+
147
+ def os_name
148
+ @os_name ||= call("uname -s").strip.downcase
149
+ end
150
+
151
+
152
+ ##
153
+ # Prompt the user for a password
154
+
155
+ def prompt_for_password
156
+ @password = ask("#{@user}@#{@host} Password:") do |q|
157
+ q.echo = false
158
+ end
159
+ end
160
+
161
+
162
+ ##
163
+ # Build an env command if an env_hash is passed
164
+
165
+ def env_cmd cmd, env_hash=@env
166
+ if env_hash && !env_hash.empty?
167
+ env_vars = env_hash.map{|e| e.join("=")}
168
+ cmd = ["env", env_vars, cmd]
169
+ end
170
+ cmd
171
+ end
172
+
173
+
174
+ ##
175
+ # Build an sh -c command
176
+
177
+ def sh_cmd string
178
+ string = string.gsub(/'/){|s| "'\\''"}
179
+
180
+ ["sh", "-c", "'#{string}'"]
181
+ end
182
+
183
+
184
+ ##
185
+ # Build a command with sudo.
186
+ # If sudo_val is nil, it is considered to mean "pass-through"
187
+ # and the default shell sudo will be used.
188
+ # If sudo_val is false, the cmd will be returned unchanged.
189
+ # If sudo_val is true, the returned command will be prefaced
190
+ # with sudo -H
191
+ # If sudo_val is a String, the command will be prefaced
192
+ # with sudo -H -u string_value
193
+
194
+ def sudo_cmd cmd, sudo_val=nil
195
+ sudo_val = sudo_val[:sudo] if Hash === sudo_val
196
+ sudo_val = @sudo if sudo_val.nil?
197
+
198
+ case sudo_val
199
+ when true
200
+ ["sudo", "-H", cmd].flatten
201
+ when String
202
+ ["sudo", "-H", "-u", sudo_val, cmd].flatten
203
+ else
204
+ cmd
205
+ end
206
+ end
207
+
208
+
209
+ ##
210
+ # Force symlinking a directory.
211
+
212
+ def symlink target, symlink_name
213
+ call "ln -sfT #{target} #{symlink_name}" rescue false
214
+ end
215
+
216
+
217
+ ##
218
+ # Synchronize a block with the current mutex if it exists.
219
+
220
+ def sync
221
+ if @mutex
222
+ @mutex.synchronize{ yield }
223
+ else
224
+ yield
225
+ end
226
+ end
227
+
228
+
229
+ ##
230
+ # Checks if timeout occurred.
231
+
232
+ def timed_out? start_time=@cmd_activity, max_time=TIMEOUT
233
+ Time.now.to_i - start_time.to_i > max_time
234
+ end
235
+
236
+
237
+ ##
238
+ # Update the time of the last command activity
239
+
240
+ def update_timeout
241
+ @cmd_activity = Time.now
242
+ end
243
+
244
+
245
+ ##
246
+ # Execute a block while setting the shell's mutex.
247
+ # Sets the mutex to its original value on exit.
248
+ # Executing commands with a mutex is used for user prompts.
249
+
250
+ def with_mutex mutex
251
+ old_mutex, @mutex = @mutex, mutex
252
+ yield
253
+ @mutex = old_mutex
254
+ end
255
+
256
+
257
+ ##
258
+ # Write string to stdout (by default).
259
+
260
+ def write str
261
+ @output.write str
262
+ end
263
+
264
+ alias << write
265
+
266
+
267
+ ##
268
+ # Execute a command with open4 and loop until the process exits.
269
+ # The cmd argument may be a string or an array. If a block is passed,
270
+ # it will be called when data is received and passed the stream type
271
+ # and stream string value:
272
+ # shell.execute "test -s 'blah' && echo 'true'" do |stream, str|
273
+ # stream #=> :stdout
274
+ # string #=> 'true'
275
+ # end
276
+ #
277
+ # The method returns the output from the stdout stream by default, and
278
+ # raises a CmdError if the exit status of the command is not zero.
279
+
280
+ def execute cmd
281
+ cmd = [cmd] unless Array === cmd
282
+ pid, inn, out, err = popen4(*cmd)
283
+
284
+ inn.sync = true
285
+ log_methods = {out => :debug, err => :error}
286
+
287
+ result, status = process_streams(pid, out, err) do |stream, data|
288
+ stream_name = stream == out ? :out : :err
289
+
290
+
291
+ # User blocks should run with sync threads to avoid badness.
292
+ sync do
293
+ Sunshine.logger.send log_methods[stream],
294
+ "#{@host}:#{stream_name}", data
295
+
296
+ yield(stream_name, data, inn) if block_given?
297
+ end
298
+
299
+
300
+ if password_required?(stream_name, data) then
301
+
302
+ kill_process(pid) unless Sunshine.interactive?
303
+
304
+ send_password_to_stream(inn, data)
305
+ end
306
+ end
307
+
308
+ raise_command_failed(status, cmd) unless status.success?
309
+
310
+ result[out].join.chomp
311
+
312
+ ensure
313
+ inn.close rescue nil
314
+ out.close rescue nil
315
+ err.close rescue nil
316
+ end
317
+
318
+
319
+ private
320
+
321
+ def raise_command_failed(status, cmd)
322
+ raise CmdError,
323
+ "Execution failed with status #{status.exitstatus}: #{[*cmd].join ' '}"
324
+ end
325
+
326
+ def password_required? stream_name, data
327
+ stream_name == :err && data =~ SUDO_PROMPT
328
+ end
329
+
330
+
331
+ def send_password_to_stream inn, data
332
+ prompt_for_password if data =~ SUDO_FAILED
333
+ inn.puts @password || prompt_for_password
334
+ end
335
+
336
+
337
+ def kill_process pid, kill_type="KILL"
338
+ begin
339
+ Process.kill kill_type, pid
340
+ Process.wait
341
+ rescue
342
+ end
343
+ end
344
+
345
+
346
+ def process_streams pid, *streams
347
+ result = Hash.new{|h,k| h[k] = []}
348
+ update_timeout
349
+
350
+ # Handle process termination ourselves
351
+ status = nil
352
+ Thread.start do
353
+ status = Process.waitpid2(pid).last
354
+ end
355
+
356
+ until streams.empty? do
357
+ # don't busy loop
358
+ selected, = select streams, nil, nil, 0.1
359
+
360
+ raise TimeoutError if timed_out?
361
+
362
+ next if selected.nil? or selected.empty?
363
+
364
+ selected.each do |stream|
365
+
366
+ update_timeout
367
+
368
+ if stream.eof? then
369
+ streams.delete stream if status # we've quit, so no more writing
370
+ next
371
+ end
372
+
373
+ data = stream.readpartial(1024)
374
+
375
+ yield(stream, data)
376
+
377
+ result[stream] << data
378
+ end
379
+ end
380
+
381
+ return result, status
382
+ end
383
+ end
384
+ end