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