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,131 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # The Output class handles all of the logging to the shell
5
+ # during the Sunshine runtime.
6
+
7
+ class Output
8
+
9
+ COLORS = {
10
+ Logger::UNKNOWN => :red,
11
+ Logger::FATAL => :red,
12
+ Logger::ERROR => :red,
13
+ Logger::WARN => :yellow,
14
+ Logger::INFO => :default,
15
+ Logger::DEBUG => :cyan,
16
+ }
17
+
18
+ attr_accessor :logger, :indent, :level
19
+
20
+ def initialize(options={})
21
+ @logger = Logger.new options[:output] || $stdout
22
+ @logger.formatter = lambda{|sev, time, progname, msg| msg}
23
+ @level = @logger.level = options[:level] || Logger::DEBUG
24
+ @indent = 0
25
+ end
26
+
27
+ ##
28
+ # Prints messages according to the standard output format.
29
+ # Options supported:
30
+ # :level: level of the log message
31
+ # :indent: indentation of the message
32
+ def print(title, message, options={})
33
+ severity = options[:level] || Logger::DEBUG
34
+ color = COLORS[severity]
35
+
36
+ options[:indent] = 0 if options[:indent] < 0
37
+ indent = " " * (options[:indent].to_i * 2)
38
+
39
+ print_string = message.split("\n")
40
+ print_string.map!{|m| "#{indent}[#{title}] #{m.chomp}"}
41
+ print_string = "#{print_string.join("\n")} \n"
42
+ print_string = print_string.foreground(color)
43
+ print_string = print_string.bright if indent.empty?
44
+
45
+ @logger.add(severity, print_string)
46
+ end
47
+
48
+ ##
49
+ # Generic log message which handles log indentation (for clarity).
50
+ # Log indentation if achieved done by passing a block:
51
+ #
52
+ # output.log("MAIN", "Main thing is happening") do
53
+ # ...
54
+ # output.log("SUB1", "Sub process thing") do
55
+ # ...
56
+ # output.log("SUB2", "Innermost process thing")
57
+ # end
58
+ # end
59
+ #
60
+ # output.log("MAIN", "Start something else")
61
+ #
62
+ # ------
63
+ # > [MAIN] Main thing is happening
64
+ # > [SUB1] Sub process thing
65
+ # > [SUB2] Innermost process thing
66
+ # >
67
+ # > [MAIN] Start something else
68
+ #
69
+ # Log level is set to the instance's default unless
70
+ # specified in the options argument with :level => Logger::LEVEL.
71
+ # The default log level is Logger::INFO
72
+ #
73
+ # Best practice for using log levels is to call the level methods
74
+ # which all work similarly to the log method:
75
+ # unknown, fatal, error, warn, info, debug
76
+ def log(title, message, options={}, &block)
77
+ unless Sunshine.trace?
78
+ return block.call if block_given?
79
+ return
80
+ end
81
+
82
+ options = {:indent => @indent}.merge(options)
83
+ self.print(title, message, options)
84
+ if block_given?
85
+ @indent = @indent + 1
86
+ begin
87
+ block.call
88
+ ensure
89
+ @indent = @indent - 1 unless @indent <= 0
90
+ @logger << "\n"
91
+ end
92
+ end
93
+ end
94
+
95
+ ##
96
+ # Log a message of log level unknown.
97
+ def unknown(title, message, options={}, &block)
98
+ self.log(title, message, options.merge(:level => Logger::UNKNOWN), &block)
99
+ end
100
+
101
+ ##
102
+ # Log a message of log level fatal.
103
+ def fatal(title, message, options={}, &block)
104
+ self.log(title, message, options.merge(:level => Logger::FATAL), &block)
105
+ end
106
+
107
+ ##
108
+ # Log a message of log level error.
109
+ def error(title, message, options={}, &block)
110
+ self.log(title, message, options.merge(:level => Logger::ERROR), &block)
111
+ end
112
+
113
+ ##
114
+ # Log a message of log level warn.
115
+ def warn(title, message, options={}, &block)
116
+ self.log(title, message, options.merge(:level => Logger::WARN), &block)
117
+ end
118
+
119
+ ##
120
+ # Log a message of log level info.
121
+ def info(title, message, options={}, &block)
122
+ self.log(title, message, options.merge(:level => Logger::INFO), &block)
123
+ end
124
+
125
+ ##
126
+ # Log a message of log level debug.
127
+ def debug(title, message, options={}, &block)
128
+ self.log(title, message, options.merge(:level => Logger::DEBUG), &block)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,48 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # The Apt dependency class supports basic apt-get features:
5
+ #
6
+ # dependency_lib.instance_eval do
7
+ # apt "ruby", :version => '1.9'
8
+ # end
9
+ #
10
+ # See the Dependency class for more info.
11
+
12
+ class Apt < Dependency
13
+
14
+ def initialize(name, options={}, &block)
15
+ super(name, options) do
16
+ pkg_name = build_pkg_name @pkg.dup, options
17
+
18
+ install "apt-get install -y #{pkg_name}"
19
+ uninstall "apt-get remove -y #{pkg_name}"
20
+
21
+ @pkg = "#{@pkg}-#{options[:version]}" if options[:version]
22
+ check_test "apt-cache search ^#{@pkg} | grep -c ^#{@pkg}", '-ge 1'
23
+
24
+ instance_eval(&block) if block_given?
25
+ end
26
+ end
27
+
28
+
29
+ private
30
+
31
+ def build_pkg_name pkg_name, options={}
32
+ pkg_name << "=#{options[:version]}" if options[:version]
33
+
34
+ pkg_name
35
+ end
36
+
37
+
38
+ def run_command(command, options={})
39
+ if @dependency_lib
40
+ if @dependency_lib.exist?('apt')
41
+ @dependency_lib.install 'apt', options
42
+ end
43
+ end
44
+
45
+ super
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,349 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Dependency objects let you define how to install, check, and remove
5
+ # a described package, including parent dependency lookup and installation.
6
+ #
7
+ # Dependency.new "ruby", :tree => dependency_lib do
8
+ # install "sudo yum install ruby"
9
+ # uninstall "sudo yum remove ruby"
10
+ # check_test "yum list installed ruby | grep -c ruby", "-ge 1"
11
+ # end
12
+ #
13
+ # Dependencies are more commonly defined through a Settler class'
14
+ # constructor methods:
15
+ #
16
+ # dependency_lib.instance_eval do
17
+ # dependency 'custom' do
18
+ # requires 'yum', 'ruby'
19
+ # install 'sudo yum install custom'
20
+ # uninstall 'sudo yum remove custom'
21
+ # check 'yum list installed custom'
22
+ # end
23
+ # end
24
+ #
25
+ # The Dependency class is simple to inherit and use as a built-in part of
26
+ # Settler (see the Yum implementation for more info):
27
+ #
28
+ # class Yum < Dependency
29
+ # def initialize(name, options={}, &block)
30
+ # super(dep_lib, name, options) do
31
+ # # Define install, check, and uninstall scripts specific to yum
32
+ # end
33
+ # end
34
+ # ...
35
+ # end
36
+ #
37
+ # Once a subclass is defined a constructor method is added automatically
38
+ # to the Settler class:
39
+ #
40
+ # dependency_lib.instance_eval do
41
+ # yum "ruby", :version => '1.9'
42
+ # end
43
+
44
+ class Dependency
45
+
46
+ class CmdError < Exception; end
47
+ class InstallError < Exception; end
48
+ class UninstallError < Exception; end
49
+
50
+
51
+ ##
52
+ # Check if sudo should be used
53
+
54
+ def self.sudo
55
+ @sudo ||= nil
56
+ end
57
+
58
+
59
+ ##
60
+ # Assign a sudo value. A value of nil means 'don't assign sudo',
61
+ # true means sudo, string means sudo -u, false means, explicitely
62
+ # don't use sudo. Yum and Gem dependency types default to sudo=true.
63
+
64
+ def self.sudo= value
65
+ @sudo = value
66
+ end
67
+
68
+
69
+ attr_reader :name, :pkg, :parents, :children
70
+
71
+ def initialize name, options={}, &block
72
+ @dependency_lib = options[:tree]
73
+
74
+ @name = name.to_s
75
+ @pkg = options[:pkg] || @name
76
+ @options = options.dup
77
+
78
+ @install = nil
79
+ @uninstall = nil
80
+ @check = nil
81
+
82
+ @parents = []
83
+ @children = []
84
+
85
+ @shell = Sunshine.shell
86
+
87
+ requires(*options[:requires]) if options[:requires]
88
+
89
+ instance_eval(&block) if block_given?
90
+ end
91
+
92
+
93
+ ##
94
+ # Append a child dependency
95
+
96
+ def add_child name
97
+ @children << name
98
+ end
99
+
100
+
101
+ ##
102
+ # Get direct child dependencies
103
+
104
+ def child_dependencies
105
+ @children
106
+ end
107
+
108
+
109
+ ##
110
+ # Define the command that checks if the dependency is installed.
111
+ # The check command must have an appropriate exitcode:
112
+ #
113
+ # dep.check "test -s 'yum list installed depname'"
114
+
115
+ def check cmd_str=nil, &block
116
+ @check = cmd_str || block
117
+ end
118
+
119
+
120
+ ##
121
+ # Define checking that the dependency is installed via unix's 'test':
122
+ #
123
+ # dep.check_test "yum list installed depname | grep -c depname", "-ge 1"
124
+
125
+ def check_test cmd_str, condition_str
126
+ check "test \"$(#{cmd_str})\" #{condition_str}"
127
+ end
128
+
129
+
130
+ ##
131
+ # Define the install command for the dependency:
132
+ #
133
+ # dep.install "yum install depname"
134
+
135
+ def install cmd=nil, &block
136
+ @install = cmd || block
137
+ end
138
+
139
+
140
+ ##
141
+ # Run the install command for the dependency
142
+ # Allows options:
143
+ # :call:: obj - an object that responds to call will be passed the bash cmd
144
+ # :skip_parents:: true - install regardless of missing parent dependencies
145
+ #
146
+ # runner = lambda{|str| system(str)}
147
+ # dep.install! :call => runner
148
+
149
+ def install! options={}
150
+ return if installed?(options)
151
+
152
+ if options[:skip_parents]
153
+ missing = missing_parents?
154
+ if missing
155
+ raise(InstallError, "Could not install #{@name}. "+
156
+ "Missing dependencies #{missing.join(", ")}")
157
+ end
158
+ else
159
+ install_parents!(options)
160
+ end
161
+
162
+ run_command(@install, options)
163
+ raise(InstallError, "Failed installing #{@name}") unless
164
+ installed?(options)
165
+ end
166
+
167
+
168
+ ##
169
+ # Call install on direct parent dependencies
170
+ # Allows options:
171
+ # :call:: obj - an object that responds to call will be passed the bash cmd
172
+ #
173
+ # runner = lambda{|str| system(str)}
174
+ # dep.install_parents! :call => runner
175
+
176
+ def install_parents! options={}
177
+ return unless @dependency_lib
178
+
179
+ @parents.each do |dep|
180
+ @dependency_lib.get(dep, options).install!(options)
181
+ end
182
+ end
183
+
184
+
185
+ ##
186
+ # Run the check command to verify that the dependency is installed
187
+ # Allows options:
188
+ # :call:: obj - an object that responds to call will be passed the bash cmd
189
+ #
190
+ # runner = lambda{|str| system(str)}
191
+ # dep.installed? :call => runner
192
+
193
+ def installed? options={}
194
+ run_command @check, options
195
+ rescue => e
196
+ false
197
+ end
198
+
199
+
200
+ ##
201
+ # Checks if any parents dependencies are missing
202
+ # Allows options:
203
+ # :call:: obj - an object that responds to call will be passed the bash cmd
204
+ #
205
+ # runner = lambda{|str| system(str)}
206
+ # dep.missing_parents? :call => runner
207
+
208
+
209
+ def missing_parents? options={}
210
+ return unless @dependency_lib
211
+
212
+ missing = []
213
+ @parents.each do |dep|
214
+ parent_dep = @dependency_lib.get dep, options
215
+
216
+ missing << dep unless parent_dep.installed?(options)
217
+
218
+ return missing if options[:limit] && options[:limit] == missing.length
219
+ end
220
+
221
+ missing.empty? ? nil : missing
222
+ end
223
+
224
+
225
+ ##
226
+ # Get direct parent dependencies
227
+
228
+ def parent_dependencies
229
+ @parents
230
+ end
231
+
232
+
233
+ ##
234
+ # Define which dependencies this dependency relies on:
235
+ #
236
+ # dep.requires 'rubygems', 'rdoc'
237
+
238
+ def requires *deps
239
+ return unless @dependency_lib
240
+
241
+ @parents.concat(deps).uniq!
242
+ deps.each do |dep|
243
+ @dependency_lib.dependencies[dep].each{|d| d.add_child(@name) }
244
+ end
245
+ end
246
+
247
+
248
+ ##
249
+ # Define the uninstall command for the dependency:
250
+ #
251
+ # dep.uninstall "yum remove depname"
252
+
253
+ def uninstall cmd=nil, &block
254
+ @uninstall = cmd || block
255
+ end
256
+
257
+
258
+ ##
259
+ # Run the uninstall command for the dependency
260
+ # Allows options:
261
+ # :call:: obj - an object that responds to call will be passed the bash cmd
262
+ # :force:: true - uninstalls regardless of child dependencies
263
+ # :remove_children:: true - removes direct child dependencies
264
+ # :remove_children:: :recursive - removes children recursively
265
+
266
+ def uninstall! options={}
267
+ if !options[:remove_children] && !options[:force]
268
+ raise UninstallError, "The #{@name} has child dependencies."
269
+ end
270
+ uninstall_children!(options) if options[:remove_children]
271
+ run_command(@uninstall, options)
272
+ raise(UninstallError, "Failed removing #{@name}") if installed?(options)
273
+ end
274
+
275
+
276
+ ##
277
+ # Removes child dependencies
278
+ # Allows options:
279
+ # :call:: obj - an object that responds to call will be passed the bash cmd
280
+ # :force:: true - uninstalls regardless of child dependencies
281
+ # :remove_children:: true - removes direct child dependencies
282
+ # :remove_children:: :recursive - removes children recursively
283
+
284
+ def uninstall_children! options={}
285
+ return unless @dependency_lib
286
+
287
+ options = options.dup
288
+
289
+ @children.each do |dep|
290
+ options.delete(:remove_children) unless
291
+ options[:remove_children] == :recursive
292
+
293
+ @dependency_lib.get(dep, options).uninstall!(options)
294
+ end
295
+ end
296
+
297
+
298
+ ##
299
+ # Alias for name
300
+
301
+ def to_s
302
+ @name
303
+ end
304
+
305
+
306
+ private
307
+
308
+ def run_command command, options={}
309
+ shell = options[:call] || @shell
310
+
311
+ if Proc === command
312
+ command.call shell, self.class.sudo
313
+
314
+ else
315
+ shell.call command, :sudo => self.class.sudo
316
+ end
317
+ end
318
+
319
+
320
+ def self.short_class_name str
321
+ str.to_s.split(":").last
322
+ end
323
+
324
+
325
+ def self.underscore str
326
+ str.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
327
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
328
+ end
329
+
330
+
331
+ def self.inherited(subclass)
332
+ class_name = short_class_name subclass.to_s
333
+ method_name = underscore class_name
334
+
335
+ DependencyLib.class_eval <<-STR, __FILE__, __LINE__ + 1
336
+ def #{method_name}(name, options={}, &block)
337
+ dep = #{class_name}.new(name, options.merge(:tree => self), &block)
338
+ self.add dep
339
+ dep
340
+ end
341
+ STR
342
+
343
+ DependencyLib.dependency_types << subclass
344
+ end
345
+
346
+ inherited self
347
+
348
+ end
349
+ end
@@ -0,0 +1,54 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # The Gem dependency class supports most of rubygem's installation features:
5
+ #
6
+ # dependency_lib.instance_eval do
7
+ # gem "rdoc", :version => '~>0.8',
8
+ # :source => 'http://gemcutter.org',
9
+ # :opts => '--use-lib blah' # Anything after --
10
+ # end
11
+ #
12
+ # See the Dependency class for more info.
13
+
14
+ class Gem < Dependency
15
+
16
+ self.sudo = true
17
+
18
+
19
+ def initialize(name, options={}, &block)
20
+ super(name, options) do
21
+ version = options[:version] ? " --version '#{options[:version]}'" : ""
22
+
23
+ source = if options[:source]
24
+ " --source #{options[:source]} --source http://gemcutter.org"
25
+ end
26
+
27
+ install_opts = " --no-ri --no-rdoc"
28
+ if options[:opts]
29
+ install_opts = "#{install_opts} -- #{options[:opts]}"
30
+ end
31
+
32
+ install "gem install #{@pkg}#{version}#{source}#{install_opts}"
33
+ uninstall "gem uninstall #{@pkg}#{version}"
34
+ check "gem list #{@pkg} -i#{version}"
35
+
36
+ requires(*options[:require].to_a) if options[:require]
37
+
38
+ instance_eval(&block) if block_given?
39
+ end
40
+ end
41
+
42
+
43
+ private
44
+
45
+ def run_command(command, options={})
46
+ if @dependency_lib
47
+ @dependency_lib.install 'rubygems', options if
48
+ @dependency_lib.exist?('rubygems')
49
+ end
50
+
51
+ super
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,62 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # The Yum dependency class supports most of yum's installation features:
5
+ #
6
+ # dependency_lib.instance_eval do
7
+ # yum "ruby", :version => '1.9',
8
+ # :rel => 'release-num',
9
+ # :arch => 'i386',
10
+ # :epoch => 'some-epoch'
11
+ # end
12
+ #
13
+ # See the Dependency class for more info.
14
+
15
+ class Yum < Dependency
16
+
17
+ self.sudo = true
18
+
19
+
20
+ def initialize(name, options={}, &block)
21
+ super(name, options) do
22
+ pkg_name = build_pkg_name @pkg.dup, options
23
+
24
+ install "yum install -y #{pkg_name}"
25
+ uninstall "yum remove -y #{pkg_name}"
26
+ check_test "yum list installed #{pkg_name} | grep -c #{@pkg}", '-ge 1'
27
+
28
+ instance_eval(&block) if block_given?
29
+ end
30
+ end
31
+
32
+
33
+ private
34
+
35
+ def build_pkg_name pkg_name, options={}
36
+ if options[:version]
37
+ pkg_name << "-#{options[:version]}"
38
+
39
+ if options[:rel]
40
+ pkg_name << "-#{options[:rel]}"
41
+
42
+ pkg_name = "#{options[:epoch]}:#{pkg_name}" if
43
+ options[:arch] && options[:epoch]
44
+ end
45
+ end
46
+
47
+ pkg_name << ".#{options[:arch]}" if options[:arch]
48
+ pkg_name
49
+ end
50
+
51
+
52
+ def run_command(command, options={})
53
+ if @dependency_lib
54
+ if @dependency_lib.exist?('yum')
55
+ @dependency_lib.install 'yum', options
56
+ end
57
+ end
58
+
59
+ super
60
+ end
61
+ end
62
+ end