sunshine 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,6 +11,10 @@ module Sunshine
11
11
 
12
12
  class Daemon
13
13
 
14
+ START_FAILED_CODE = 10
15
+ STOP_FAILED_CODE = 11
16
+ RESTART_FAILED_CODE = 12
17
+ STATUS_DOWN_CODE = 13
14
18
 
15
19
  ##
16
20
  # Returns an array of method names to assign to the binder
@@ -142,7 +146,7 @@ module Sunshine
142
146
  binder = config_binding server_app.shell
143
147
 
144
148
  configure_remote_dirs server_app.shell
145
- touch_log_files server_app.shell
149
+ chown_log_files server_app.shell
146
150
 
147
151
  yield(server_app, binder) if block_given?
148
152
 
@@ -156,7 +160,7 @@ module Sunshine
156
160
  @setup_successful = true
157
161
 
158
162
  rescue => e
159
- raise CriticalDeployError.new(e, "Could not setup #{@name}")
163
+ raise DaemonError.new(e, "Could not setup #{@name}")
160
164
  end
161
165
 
162
166
 
@@ -186,12 +190,12 @@ module Sunshine
186
190
 
187
191
  each_server_app do |server_app|
188
192
  begin
189
- server_app.shell.call start_cmd,
193
+ server_app.shell.call _start_cmd,
190
194
  :sudo => pick_sudo(server_app.shell)
191
195
 
192
196
  yield(server_app) if block_given?
193
197
  rescue => e
194
- raise CriticalDeployError.new(e, "Could not start #{@name}")
198
+ raise DaemonError.new(e, "Could not start #{@name}")
195
199
  end
196
200
  end
197
201
  end
@@ -203,7 +207,7 @@ module Sunshine
203
207
 
204
208
  def status
205
209
  each_server_app do |server_app|
206
- server_app.shell.call status_cmd, :sudo => pick_sudo(server_app.shell)
210
+ server_app.shell.call _status_cmd, :sudo => pick_sudo(server_app.shell)
207
211
  end
208
212
  true
209
213
 
@@ -220,12 +224,12 @@ module Sunshine
220
224
 
221
225
  each_server_app do |server_app|
222
226
  begin
223
- server_app.shell.call stop_cmd,
227
+ server_app.shell.call _stop_cmd,
224
228
  :sudo => pick_sudo(server_app.shell)
225
229
 
226
230
  yield(server_app) if block_given?
227
231
  rescue => e
228
- raise CriticalDeployError.new(e, "Could not stop #{@name}")
232
+ raise DaemonError.new(e, "Could not stop #{@name}")
229
233
  end
230
234
  end
231
235
  end
@@ -242,25 +246,42 @@ module Sunshine
242
246
  Sunshine.logger.info @name, "Restarting #{@name} daemon" do
243
247
  each_server_app do |server_app|
244
248
  begin
245
- server_app.shell.call restart_cmd,
249
+ server_app.shell.call _restart_cmd,
246
250
  :sudo => pick_sudo(server_app.shell)
247
251
 
248
252
  yield(server_app) if block_given?
249
253
  rescue => e
250
- raise CriticalDeployError.new(e, "Could not restart #{@name}")
254
+ raise DaemonError.new(e, "Could not restart #{@name}")
251
255
  end
252
256
  end
253
257
  end
254
258
  end
255
259
 
256
260
 
261
+ ##
262
+ # Wrap a command with a fail-specific exitcode and message.
263
+
264
+ def exit_on_failure cmd, exitcode=1, message=nil
265
+ "(#{cmd}) || (echo '#{message}' && exit #{exitcode});"
266
+ end
267
+
268
+
257
269
  ##
258
270
  # Gets the command that starts the daemon.
259
271
  # Should be overridden by child classes.
260
272
 
261
273
  def start_cmd
262
- return @start_cmd ||
263
- raise(CriticalDeployError, "@start_cmd undefined. Can't start #{@name}")
274
+ return @start_cmd if @start_cmd
275
+ raise DaemonError, "start_cmd undefined for #{@name}"
276
+ end
277
+
278
+
279
+ ##
280
+ # Start command wrapped with an exit_on_failure handler.
281
+
282
+ def _start_cmd
283
+ exit_on_failure start_cmd, START_FAILED_CODE,
284
+ "Could not start #{@name} for #{@app.name}"
264
285
  end
265
286
 
266
287
 
@@ -269,26 +290,59 @@ module Sunshine
269
290
 
270
291
  def stop_cmd
271
292
  "test -f #{@pid} && kill -#{@sigkill} $(cat #{@pid}) && sleep 1 && "+
272
- "rm -f #{@pid} || echo 'Could not kill #{@name} pid for #{@app.name}';"
293
+ "rm -f #{@pid}"
294
+ end
295
+
296
+
297
+ ##
298
+ # Stop command wrapped with an exit_on_failure handler.
299
+
300
+ def _stop_cmd
301
+ exit_on_failure stop_cmd, STOP_FAILED_CODE,
302
+ "Could not kill #{@name} pid for #{@app.name}"
273
303
  end
274
304
 
275
305
 
276
306
  ##
277
307
  # Gets the command that restarts the daemon.
308
+ # Should be overridden by child classes if different
309
+ # from start_cmd && stop_cmd.
278
310
 
279
311
  def restart_cmd
280
- @restart_cmd || [stop_cmd, start_cmd].map{|c| "(#{c})"}.join(" && ")
312
+ @restart_cmd
281
313
  end
282
314
 
283
315
 
284
316
  ##
285
- # Get the command to check if the daemon is running.
317
+ # Restart command wrapped with an exit_on_failure handler.
318
+
319
+ def _restart_cmd
320
+ if restart_cmd
321
+ exit_on_failure restart_cmd, RESTART_FAILED_CODE,
322
+ "Could not restart #{@name} for #{@app.name}"
323
+ else
324
+ "(#{_stop_cmd}) && (#{_start_cmd});"
325
+ end
326
+ end
327
+
328
+
329
+ ##
330
+ # Status command wrapped with an exit_on_failure handler.
286
331
 
287
332
  def status_cmd
288
333
  @status_cmd || "test -f #{@pid} && kill -0 $(cat #{@pid})"
289
334
  end
290
335
 
291
336
 
337
+ ##
338
+ # Get the command to check if the daemon is running.
339
+
340
+ def _status_cmd
341
+ exit_on_failure status_cmd, STATUS_DOWN_CODE,
342
+ "#{@app.name} #{@name} is not running"
343
+ end
344
+
345
+
292
346
  ##
293
347
  # Append or override daemon log file paths:
294
348
  # daemon.log_files :stderr => "/all_logs/stderr.log"
@@ -390,7 +444,7 @@ module Sunshine
390
444
  ##
391
445
  # Make sure log files are owned by the daemon's user.
392
446
 
393
- def touch_log_files shell
447
+ def chown_log_files shell
394
448
  files = @log_files.values.join(" ")
395
449
 
396
450
  sudo = pick_sudo(shell)
@@ -401,8 +455,8 @@ module Sunshine
401
455
  nil
402
456
  end
403
457
 
404
- shell.call "touch #{files}", :sudo => true
405
- shell.call "chown #{user} #{files}", :sudo => true if user
458
+ return unless user
459
+ shell.call "chown -f #{user} #{files}", :sudo => true rescue nil
406
460
  end
407
461
 
408
462
 
@@ -419,7 +473,7 @@ module Sunshine
419
473
  %w{start stop restart status}.each do |script|
420
474
  script_file = "#{@config_path}/#{script}"
421
475
 
422
- cmd = send "#{script}_cmd".to_sym
476
+ cmd = send "_#{script}_cmd"
423
477
 
424
478
  sa.shell.make_file script_file, cmd,
425
479
  :flags => '--chmod=ugo=rwx'
@@ -36,8 +36,6 @@ module Sunshine
36
36
 
37
37
  class DependencyLib
38
38
 
39
- class MissingDependency < Exception; end
40
-
41
39
  ##
42
40
  # Array of all dependency classes. Appended to automatically when
43
41
  # DependencyLib::Dependency is inherited.
@@ -17,38 +17,56 @@ module Sunshine
17
17
 
18
18
  ##
19
19
  # An error occurred when attempting to run a command on the local system
20
- class CmdError < Exception; end
20
+ class CmdError < Exception;
21
+ attr_reader :exit_code
21
22
 
22
-
23
- ##
24
- # An ssh call returned a non-zero exit code
25
- class SSHCmdError < CmdError
26
- attr_reader :shell
27
- def initialize message=nil, shell=nil
28
- @shell = shell
29
- super(message)
23
+ def initialize exit_code, cmd=nil
24
+ message = "Execution failed with status #{exit_code}: #{cmd}"
25
+ super message
26
+ @exit_code = exit_code
30
27
  end
31
28
  end
32
29
 
33
30
 
31
+ ##
32
+ # A shell command timed out.
33
+ class TimeoutError < Exception; end
34
+
35
+
36
+ ##
37
+ # Remote connection to server failed.
38
+ class ConnectionError < Exception; end
39
+
40
+
34
41
  ##
35
42
  # Something went wrong with a deploy-specific item.
36
43
  class DeployError < Exception; end
37
44
 
38
45
 
39
46
  ##
40
- # The error is serious enough that deploy cannot proceed.
41
- # Sunshine will attempt to revert to a previous deploy if available.
42
- class CriticalDeployError < DeployError; end
47
+ # Something went wrong with a daemon-specific item.
48
+ class DaemonError < Exception; end
43
49
 
44
50
 
45
51
  ##
46
- # The error is so serious that all no more action can be taken.
47
- # Sunshine will attempt to close any ssh connections and stop the deploy.
48
- class FatalDeployError < DeployError; end
52
+ # Something went wrong with a dependency-specific item.
53
+ class DependencyError < Exception; end
54
+
55
+
56
+ ##
57
+ # Dependency requested could not be found.
58
+ class MissingDependency < DependencyError; end
59
+
49
60
 
50
61
  ##
51
- # A dependency could not be installed.
52
- class DependencyError < FatalDeployError; end
62
+ # Dependency failed to install.
63
+ class InstallError < DependencyError; end
53
64
 
65
+ ##
66
+ # Dependency failed to uninstall.
67
+ class UninstallError < DependencyError; end
68
+
69
+ ##
70
+ # Something went wrong with a scm-specific item.
71
+ class RepoError < Exception; end
54
72
  end
@@ -32,7 +32,7 @@ module Sunshine
32
32
  # Checks if dependency type is valid for a given shell.
33
33
 
34
34
  def self.system_manager? shell=nil
35
- (shell || Sunshine.shell).syscall "apt-get --version"
35
+ (shell || Sunshine.shell).system "apt-get --version"
36
36
  end
37
37
 
38
38
 
@@ -43,11 +43,6 @@ module Sunshine
43
43
 
44
44
  class Dependency
45
45
 
46
- class CmdError < Exception; end
47
- class InstallError < Exception; end
48
- class UninstallError < Exception; end
49
-
50
-
51
46
  ##
52
47
  # Check if sudo should be used
53
48
 
@@ -34,7 +34,7 @@ module Sunshine
34
34
  # Checks if dependency type is valid for a given shell.
35
35
 
36
36
  def self.system_manager? shell=nil
37
- (shell || Sunshine.shell).syscall "yum --version"
37
+ (shell || Sunshine.shell).system "yum --version"
38
38
  end
39
39
 
40
40
 
@@ -11,8 +11,6 @@ module Sunshine
11
11
 
12
12
  class RemoteShell < Shell
13
13
 
14
- class ConnectionError < FatalDeployError; end
15
-
16
14
  ##
17
15
  # The loop to keep the ssh connection open.
18
16
  LOGIN_LOOP = "echo ok; echo ready; "+
@@ -61,7 +59,7 @@ module Sunshine
61
59
 
62
60
  @user ||= options[:user]
63
61
 
64
- @rsync_flags = ["-azP"]
62
+ @rsync_flags = ["-azrP"]
65
63
  @rsync_flags.concat [*options[:rsync_flags]] if options[:rsync_flags]
66
64
 
67
65
  @ssh_flags = [
@@ -170,7 +168,25 @@ module Sunshine
170
168
  # Checks if the given file exists
171
169
 
172
170
  def file? filepath
173
- syscall "test -f #{filepath}"
171
+ self.system "test -f #{filepath}"
172
+ end
173
+
174
+
175
+ ##
176
+ # Start an interactive shell with preset permissions and env.
177
+ # Optionally pass a command to be run first.
178
+
179
+ def tty! cmd=nil
180
+ sync do
181
+ cmd = [cmd, "sh -il"].compact.join " && "
182
+ cmd = quote_cmd cmd
183
+
184
+ pid = fork do
185
+ exec \
186
+ ssh_cmd(sudo_cmd(env_cmd(cmd)), :flags => "-t").to_a.join(" ")
187
+ end
188
+ Process.waitpid pid
189
+ end
174
190
  end
175
191
 
176
192
 
@@ -202,7 +218,7 @@ module Sunshine
202
218
 
203
219
 
204
220
  ##
205
- # Uploads a file via rsync
221
+ # Uploads a file via rsync.
206
222
 
207
223
  def upload from_path, to_path, options={}, &block
208
224
  to_path = "#{@host}:#{to_path}"
@@ -1,7 +1,5 @@
1
1
  module Sunshine
2
2
 
3
- class RepoError < Exception; end
4
-
5
3
  ##
6
4
  # An abstract class to wrap simple basic scm features. The primary function
7
5
  # of repo objects is to get information about the scm branch that is being
@@ -80,12 +78,12 @@ module Sunshine
80
78
  # Defaults to false. Subclasses must override this method to enable
81
79
  # auto detecting of a given scm implementation.
82
80
 
83
- def self.valid?
81
+ def self.valid? *args
84
82
  false
85
83
  end
86
84
 
87
85
 
88
- attr_reader :url, :scm
86
+ attr_accessor :url, :scm, :flags
89
87
 
90
88
  def initialize url, options={}
91
89
  @scm = self.class.name.split("::").last.sub('Repo', '').downcase
@@ -106,11 +104,12 @@ module Sunshine
106
104
  shell.call "test -d #{path} && rm -rf #{path} || echo false"
107
105
  shell.call "mkdir -p #{path}"
108
106
 
109
- do_checkout path, shell
110
- get_repo_info path, shell
107
+ do_checkout path, shell
108
+ get_repo_info path, shell
111
109
  end
112
110
 
113
111
 
112
+
114
113
  ##
115
114
  # Checkout the repo - implemented by subclass
116
115
 
@@ -12,7 +12,7 @@ module Sunshine
12
12
 
13
13
  def initialize url, options={}
14
14
  super
15
- @flags << '-r' << '--exclude .svn/' << '--exclude .git/'
15
+ @flags << '--exclude .svn/' << '--exclude .git/'
16
16
  @url << "/" unless @url[-1..-1] == "/"
17
17
  end
18
18
 
@@ -33,7 +33,7 @@ module Sunshine
33
33
 
34
34
 
35
35
  ##
36
- # Define an attribue that will get a value from app, or locally if
36
+ # Define an attribute that will get a value from app, or locally if
37
37
  # @app isn't set.
38
38
 
39
39
  def self.app_attr *attribs
@@ -74,6 +74,23 @@ module Sunshine
74
74
  end
75
75
 
76
76
 
77
+ ##
78
+ # Creates a ServerApp instance from a deploy info file.
79
+
80
+ def self.from_info_file path, shell=nil
81
+ shell ||= Sunshine.shell
82
+
83
+ opts = YAML.load shell.call("cat #{path}")
84
+ opts[:root_path] = opts.delete :path
85
+
86
+ sa_shell = shell.dup
87
+ sa_shell.env = opts[:env] || Hash.new
88
+ sa_shell.connect if shell.connected?
89
+
90
+ new opts[:name], sa_shell, opts
91
+ end
92
+
93
+
77
94
  app_attr :name, :deploy_name
78
95
  app_attr :root_path, :checkout_path, :current_path
79
96
  app_attr :deploys_path, :log_path, :shared_path, :scripts_path
@@ -81,6 +98,13 @@ module Sunshine
81
98
  attr_accessor :app, :roles, :scripts, :info, :shell, :crontab, :health
82
99
  attr_writer :pkg_manager
83
100
 
101
+ ##
102
+ # Create a server app instance. Supports the following
103
+ # argument configurations:
104
+ #
105
+ # ServerApp.new app_inst, "myserver.com", :roles => :web
106
+ # ServerApp.new "app_name", shell_inst, options_hash
107
+
84
108
  def initialize app, host, options={}
85
109
 
86
110
  @app = App === app ? app : nil
@@ -108,6 +132,9 @@ module Sunshine
108
132
 
109
133
  @crontab = Crontab.new name, @shell
110
134
  @health = Healthcheck.new shared_path, @shell
135
+
136
+ @all_deploy_names = nil
137
+ @previous_deploy_name = nil
111
138
  end
112
139
 
113
140
 
@@ -140,7 +167,7 @@ module Sunshine
140
167
  end
141
168
 
142
169
  if build_scripts[:status].empty?
143
- build_scripts[:status] << "echo 'No daemons for #{self.name}'; exit 1;"
170
+ build_scripts[:status] << "echo 'No status for #{self.name}'; exit 1;"
144
171
  end
145
172
 
146
173
  build_scripts.each do |name, cmds|
@@ -152,6 +179,8 @@ module Sunshine
152
179
 
153
180
  write_script name, bash
154
181
  end
182
+
183
+ symlink_scripts_to_root
155
184
  end
156
185
 
157
186
 
@@ -194,7 +223,7 @@ module Sunshine
194
223
  def deploy_details reload=false
195
224
  return @deploy_details if @deploy_details && !reload
196
225
  @deploy_details =
197
- YAML.load @shell.call("cat #{self.scripts_path}/info") rescue nil
226
+ YAML.load @shell.call("cat #{self.root_path}/info") rescue nil
198
227
 
199
228
  @deploy_details = nil unless Hash === @deploy_details
200
229
 
@@ -234,8 +263,11 @@ module Sunshine
234
263
  :deployed_as => @shell.call("whoami"),
235
264
  :deployed_by => Sunshine.shell.user,
236
265
  :deploy_name => File.basename(self.checkout_path),
266
+ :name => self.name,
267
+ :env => shell_env,
237
268
  :roles => @roles,
238
- :path => self.root_path
269
+ :path => self.root_path,
270
+ :sunshine_version => Sunshine::VERSION
239
271
  }.merge @info
240
272
  end
241
273
 
@@ -348,6 +380,31 @@ fi
348
380
  end
349
381
 
350
382
 
383
+ ##
384
+ # Returns an array of all deploys in the deploys_path dir,
385
+ # starting with the oldest.
386
+
387
+ def all_deploy_names reload=false
388
+ return @all_deploy_names if @all_deploy_names && !reload
389
+
390
+ @all_deploy_names =
391
+ @shell.call("ls -rc1 #{self.deploys_path}").split("\n")
392
+ end
393
+
394
+
395
+ ##
396
+ # Returns the name of the previous deploy.
397
+
398
+ def previous_deploy_name reload=false
399
+ return @previous_deploy_name if @previous_deploy_name && !reload
400
+
401
+ arr = all_deploy_names(reload)
402
+ arr.delete(@deploy_name)
403
+
404
+ @previous_deploy_name = arr.last
405
+ end
406
+
407
+
351
408
  ##
352
409
  # Run a rake task the deploy server.
353
410
 
@@ -370,7 +427,7 @@ fi
370
427
  # based on Sunshine's max_deploy_versions.
371
428
 
372
429
  def remove_old_deploys
373
- deploys = @shell.call("ls -1 #{self.deploys_path}").split("\n")
430
+ deploys = all_deploy_names true
374
431
 
375
432
  return unless deploys.length > Sunshine.max_deploy_versions
376
433
 
@@ -388,7 +445,18 @@ fi
388
445
  # Post-deploy only.
389
446
 
390
447
  def restart
391
- @shell.call "#{self.root_path}/restart" rescue false
448
+ # Permissions are handled by the script, use: :sudo => false
449
+ run_script :stop, :sudo => false
450
+ end
451
+
452
+
453
+ ##
454
+ # Run the app's restart script. Raises an exception on failure.
455
+ # Post-deploy only.
456
+
457
+ def restart!
458
+ # Permissions are handled by the script, use: :sudo => false
459
+ run_script! :restart, :sudo => false
392
460
  end
393
461
 
394
462
 
@@ -399,7 +467,7 @@ fi
399
467
  def revert!
400
468
  @shell.call "rm -rf #{self.checkout_path}"
401
469
 
402
- last_deploy = @shell.call("ls -rc1 #{self.deploys_path}").split("\n").last
470
+ last_deploy = previous_deploy_name(true)
403
471
 
404
472
  if last_deploy && !last_deploy.empty?
405
473
  @shell.symlink "#{self.deploys_path}/#{last_deploy}", self.current_path
@@ -438,13 +506,25 @@ fi
438
506
 
439
507
 
440
508
  ##
441
- # Runs a script from the script_path.
509
+ # Runs a script from the root_path.
442
510
  # Post-deploy only.
443
511
 
444
512
  def run_script name, options=nil, &block
445
513
  options ||= {}
446
- @shell.call \
447
- File.join(self.scripts_path, name.to_s), options, &block rescue false
514
+ run_script! name, options, &block rescue false
515
+ end
516
+
517
+
518
+ ##
519
+ # Runs a script from the root_path. Raises an exception if the status
520
+ # code is not 0.
521
+ # Post-deploy only.
522
+
523
+ def run_script! name, options=nil, &block
524
+ options ||= {}
525
+
526
+ script_path = File.join self.root_path, name.to_s
527
+ @shell.call script_path, options, &block
448
528
  end
449
529
 
450
530
 
@@ -454,7 +534,12 @@ fi
454
534
 
455
535
  def running?
456
536
  # Permissions are handled by the script, use: :sudo => false
457
- run_script :status, :sudo => false
537
+ run_script! :status, :sudo => false
538
+ true
539
+
540
+ rescue CmdError => e
541
+ return false if e.exit_code == Daemon::STATUS_DOWN_CODE
542
+ raise e
458
543
  end
459
544
 
460
545
 
@@ -499,6 +584,23 @@ fi
499
584
  end
500
585
 
501
586
 
587
+ ##
588
+ # Run the app's start script. Raises an exception on failure.
589
+ # Post-deploy only.
590
+
591
+ def start! options=nil
592
+ options ||= {}
593
+
594
+ if running?
595
+ return unless options[:force]
596
+ stop!
597
+ end
598
+
599
+ # Permissions are handled by the script, use: :sudo => false
600
+ run_script! :start, :sudo => false
601
+ end
602
+
603
+
502
604
  ##
503
605
  # Get the app's status: :running or :down.
504
606
 
@@ -517,6 +619,16 @@ fi
517
619
  end
518
620
 
519
621
 
622
+ ##
623
+ # Run the app's stop script. Raises an exception on failure.
624
+ # Post-deploy only.
625
+
626
+ def stop!
627
+ # Permissions are handled by the script, use: :sudo => false
628
+ run_script! :stop, :sudo => false
629
+ end
630
+
631
+
520
632
  ##
521
633
  # Creates a symlink to the app's checkout path.
522
634
 
@@ -525,12 +637,34 @@ fi
525
637
  end
526
638
 
527
639
 
640
+ ##
641
+ # Creates a symlink of every script in the scripts_path dir in the
642
+ # app's root directory for easy access.
643
+
644
+ def symlink_scripts_to_root
645
+ scripts = @shell.call("ls -1 #{self.scripts_path}").split("\n")
646
+
647
+ scripts.each do |name|
648
+ script_file = File.join self.scripts_path, name
649
+ pointer_file = File.join self.root_path, name
650
+
651
+ @shell.symlink script_file, pointer_file
652
+ end
653
+ end
654
+
655
+
528
656
  ##
529
657
  # Assumes the passed code_dir is the root directory of the checked out
530
658
  # codebase and uploads it to the checkout_path.
531
659
 
532
660
  def upload_codebase code_dir, scm_info={}
533
- RsyncRepo.new(code_dir).checkout_to self.checkout_path, @shell
661
+ excludes = scm_info.delete :exclude if scm_info[:exclude]
662
+ excludes = [excludes].flatten.compact
663
+ excludes.map!{|e| "--exclude #{e}"}
664
+
665
+ repo = RsyncRepo.new code_dir, :flags => excludes
666
+ repo.checkout_to self.checkout_path, @shell
667
+
534
668
  @info[:scm] = scm_info
535
669
  end
536
670
 
@@ -583,8 +717,6 @@ fi
583
717
 
584
718
  @shell.make_file script_file, contents,
585
719
  :flags => '--chmod=ugo=rwx' unless @shell.file? script_file
586
-
587
- @shell.symlink script_file, "#{self.root_path}/#{name}"
588
720
  end
589
721
 
590
722