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.
- data/History.txt +237 -0
- data/Manifest.txt +70 -0
- data/README.txt +277 -0
- data/Rakefile +46 -0
- data/bin/sunshine +5 -0
- data/examples/deploy.rb +61 -0
- data/examples/deploy_tasks.rake +112 -0
- data/examples/standalone_deploy.rb +31 -0
- data/lib/commands/add.rb +96 -0
- data/lib/commands/default.rb +169 -0
- data/lib/commands/list.rb +322 -0
- data/lib/commands/restart.rb +62 -0
- data/lib/commands/rm.rb +83 -0
- data/lib/commands/run.rb +151 -0
- data/lib/commands/start.rb +72 -0
- data/lib/commands/stop.rb +61 -0
- data/lib/sunshine/app.rb +876 -0
- data/lib/sunshine/binder.rb +70 -0
- data/lib/sunshine/crontab.rb +143 -0
- data/lib/sunshine/daemon.rb +380 -0
- data/lib/sunshine/daemons/ar_sendmail.rb +28 -0
- data/lib/sunshine/daemons/delayed_job.rb +30 -0
- data/lib/sunshine/daemons/nginx.rb +104 -0
- data/lib/sunshine/daemons/rainbows.rb +35 -0
- data/lib/sunshine/daemons/server.rb +66 -0
- data/lib/sunshine/daemons/unicorn.rb +26 -0
- data/lib/sunshine/dependencies.rb +103 -0
- data/lib/sunshine/dependency_lib.rb +200 -0
- data/lib/sunshine/exceptions.rb +54 -0
- data/lib/sunshine/healthcheck.rb +83 -0
- data/lib/sunshine/output.rb +131 -0
- data/lib/sunshine/package_managers/apt.rb +48 -0
- data/lib/sunshine/package_managers/dependency.rb +349 -0
- data/lib/sunshine/package_managers/gem.rb +54 -0
- data/lib/sunshine/package_managers/yum.rb +62 -0
- data/lib/sunshine/remote_shell.rb +241 -0
- data/lib/sunshine/repo.rb +128 -0
- data/lib/sunshine/repos/git_repo.rb +122 -0
- data/lib/sunshine/repos/rsync_repo.rb +29 -0
- data/lib/sunshine/repos/svn_repo.rb +78 -0
- data/lib/sunshine/server_app.rb +554 -0
- data/lib/sunshine/shell.rb +384 -0
- data/lib/sunshine.rb +391 -0
- data/templates/logrotate/logrotate.conf.erb +11 -0
- data/templates/nginx/nginx.conf.erb +109 -0
- data/templates/nginx/nginx_optimize.conf +23 -0
- data/templates/nginx/nginx_proxy.conf +13 -0
- data/templates/rainbows/rainbows.conf.erb +18 -0
- data/templates/tasks/sunshine.rake +114 -0
- data/templates/unicorn/unicorn.conf.erb +6 -0
- data/test/fixtures/app_configs/test_app.yml +11 -0
- data/test/fixtures/sunshine_test/test_upload +0 -0
- data/test/mocks/mock_object.rb +179 -0
- data/test/mocks/mock_open4.rb +117 -0
- data/test/test_helper.rb +188 -0
- data/test/unit/test_app.rb +489 -0
- data/test/unit/test_binder.rb +20 -0
- data/test/unit/test_crontab.rb +128 -0
- data/test/unit/test_git_repo.rb +26 -0
- data/test/unit/test_healthcheck.rb +70 -0
- data/test/unit/test_nginx.rb +107 -0
- data/test/unit/test_rainbows.rb +26 -0
- data/test/unit/test_remote_shell.rb +102 -0
- data/test/unit/test_repo.rb +42 -0
- data/test/unit/test_server.rb +324 -0
- data/test/unit/test_server_app.rb +425 -0
- data/test/unit/test_shell.rb +97 -0
- data/test/unit/test_sunshine.rb +157 -0
- data/test/unit/test_svn_repo.rb +55 -0
- data/test/unit/test_unicorn.rb +22 -0
- 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
|