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,322 @@
1
+ require 'json'
2
+
3
+ module Sunshine
4
+
5
+ ##
6
+ # List and perform simple state actions on lists of sunshine apps.
7
+ #
8
+ # Usage: sunshine list app_name [more names...] [options]
9
+ #
10
+ # Arguments:
11
+ # app_name Name of an application to list.
12
+ #
13
+ # Options:
14
+ # -s, --status Check if an app is running.
15
+ # -d, --details Get details about the deployed apps.
16
+ # -h, --health [STATUS] Set or get the healthcheck status.
17
+ # (enable, disable, remove)
18
+ # -f, --format FORMAT Set the output format (txt, yml, json)
19
+ # -u, --user USER User to use for remote login. Use with -r
20
+ # -r, --remote svr1,svr2 Run on one or more remote servers.
21
+ # -v, --verbose Run in verbose mode.
22
+
23
+ class ListCommand < DefaultCommand
24
+
25
+ ##
26
+ # Takes an array and a hash, runs the command and returns:
27
+ # true: success
28
+ # false: failed
29
+ # exitcode:
30
+ # code == 0: success
31
+ # code != 0: failed
32
+ # and optionally an accompanying message.
33
+
34
+ def self.exec names, config
35
+ action = config['return'] || :exist?
36
+
37
+ args = config[action.to_s] || []
38
+ args = [args, names].flatten
39
+
40
+ output = exec_each_server config do |shell|
41
+ new(shell).send(action, *args)
42
+ end
43
+
44
+ return output
45
+ end
46
+
47
+
48
+ ##
49
+ # Executes common list functionality for each deploy server.
50
+
51
+ def self.exec_each_server config
52
+ shells = config['servers']
53
+ format = config['format']
54
+
55
+ responses = {}
56
+ success = true
57
+
58
+ shells.each do |shell|
59
+ shell.connect
60
+
61
+ begin
62
+ state, response = yield(shell)
63
+ rescue => e
64
+ state = false
65
+ response = "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
66
+ end
67
+
68
+ host = shell.host
69
+ success = state if success
70
+ responses[host] = build_response state, response
71
+
72
+ shell.disconnect
73
+ end
74
+
75
+ output = format ? self.send(format, responses) : responses
76
+ return success, output
77
+ end
78
+
79
+
80
+ ##
81
+ # Formats response as text output:
82
+ # ------------------
83
+ # subdomain.host.com
84
+ # ------------------
85
+ # app_name: running
86
+
87
+ def self.txt_format res_hash
88
+ str_out = ""
89
+
90
+ res_hash.each do |host, response|
91
+ separator = "-" * host.length
92
+
93
+ host_status = if Hash === response[:data]
94
+ apps_status = response[:data].map do |app_name, status|
95
+ "#{app_name}: #{status[:data]}\n"
96
+ end
97
+ apps_status.join("\n")
98
+
99
+ else
100
+ response[:data]
101
+ end
102
+
103
+ str_out << "\n"
104
+ str_out << [separator, host, separator].join("\n")
105
+ str_out << "\n"
106
+ str_out << host_status
107
+ str_out << "\n"
108
+ end
109
+
110
+ str_out
111
+ end
112
+
113
+
114
+ ##
115
+ # Formats response as yaml:
116
+
117
+ def self.yml_format res_hash
118
+ res_hash.to_yaml
119
+ end
120
+
121
+
122
+ ##
123
+ # Formats response as json:
124
+
125
+ def self.json_format res_hash
126
+ res_hash.to_json
127
+ end
128
+
129
+
130
+
131
+ attr_accessor :app_list, :shell
132
+
133
+ def initialize shell
134
+ @shell = shell
135
+ @app_list = self.class.load_list @shell
136
+ end
137
+
138
+
139
+ ##
140
+ # Reads and returns the specified apps' info file.
141
+ # Returns a response hash (see ListCommand#each_app).
142
+
143
+ def details(*app_names)
144
+ each_app(*app_names) do |server_app|
145
+ "\n#{server_app.deploy_details.to_yaml}"
146
+ end
147
+ end
148
+
149
+
150
+ ##
151
+ # Returns the path of specified apps.
152
+ # Returns a response hash (see ListCommand#each_app).
153
+
154
+ def exist?(*app_names)
155
+ each_app(*app_names) do |server_app|
156
+ server_app.root_path
157
+ end
158
+ end
159
+
160
+
161
+ ##
162
+ # Get or set the healthcheck state.
163
+ # Returns a response hash (see ListCommand#each_app).
164
+
165
+ def health(*app_names)
166
+ action = app_names.delete_at(0) if Symbol === app_names.first
167
+
168
+ each_app(*app_names) do |server_app|
169
+ server_app.health.send action if action
170
+ server_app.health.status
171
+ end
172
+ end
173
+
174
+
175
+ ##
176
+ # Checks if the apps' pids are present.
177
+ # Returns a response hash (see ListCommand#each_app).
178
+
179
+ def status(*app_names)
180
+ each_app(*app_names) do |server_app|
181
+ server_app.status
182
+ end
183
+ end
184
+
185
+
186
+ ##
187
+ # Runs a command and returns the status for each app_name:
188
+ # status_after_command 'restart', ['app1', 'app2']
189
+
190
+ def status_after_command cmd, app_names
191
+ each_app(*app_names) do |server_app|
192
+
193
+ yield(server_app) if block_given?
194
+
195
+ begin
196
+ server_app.send cmd.to_sym
197
+ server_app.running? ? 'running' : 'down'
198
+
199
+ rescue CmdError => e
200
+ raise "Failed running #{cmd}: #{server_app.status}"
201
+ end
202
+ end
203
+ end
204
+
205
+
206
+ # Do something with each server app and build a response hash:
207
+ # each_app do |server_app|
208
+ # ...
209
+ # end
210
+ #
211
+ # Restrict it to a set of apps if they are present on the server:
212
+ # each_app('app1', 'app2') do |server_app|
213
+ # ...
214
+ # end
215
+ #
216
+ # Returns a response hash:
217
+ # {"app_name" => {:success => true, :data => "somedata"} ...}
218
+
219
+ def each_app(*app_names)
220
+ app_names = @app_list.keys if app_names.empty?
221
+
222
+ response_for_each(*app_names) do |name|
223
+ path = @app_list[name]
224
+
225
+ raise "Application not found." unless path
226
+
227
+ server_app = ServerApp.new name, @shell, :root_path => path
228
+
229
+ yield(server_app) if block_given?
230
+ end
231
+ end
232
+
233
+
234
+ ##
235
+ # Builds a response object for each item passed and returns
236
+ # the result of the passed block as its data value.
237
+
238
+ def response_for_each(*items)
239
+ response = {}
240
+ success = true
241
+
242
+ items.each do |item|
243
+
244
+ begin
245
+ data = yield(item) if block_given?
246
+
247
+ response[item] = self.class.build_response true, data
248
+
249
+ rescue => e
250
+ success = false
251
+ response[item] = self.class.build_response false, e.message
252
+ end
253
+
254
+ end
255
+
256
+ [success, response]
257
+ end
258
+
259
+
260
+ ##
261
+ # Builds a standard response entry:
262
+ # {:success => true, :data => "somedata"}
263
+
264
+ def self.build_response success, data=nil
265
+ {:success => success, :data => data}
266
+ end
267
+
268
+
269
+ ##
270
+ # Load the app list yaml file from the server.
271
+
272
+ def self.load_list server
273
+ list = YAML.load(server.call(Sunshine::READ_LIST_CMD))
274
+ list = {} unless Hash === list
275
+ list
276
+ end
277
+
278
+
279
+ ##
280
+ # Write the app list hash to the remote server.
281
+
282
+ def self.save_list list, server
283
+ path = server.expand_path Sunshine::APP_LIST_PATH
284
+ server.make_file path, list.to_yaml
285
+ end
286
+
287
+
288
+ ##
289
+ # Parses the argv passed to the command
290
+
291
+ def self.parse_args argv
292
+ parse_remote_args(argv) do |opt, options|
293
+ opt.banner = <<-EOF
294
+
295
+ Usage: #{opt.program_name} list app_name [more names...] [options]
296
+
297
+ Arguments:
298
+ app_name Name of an application to list.
299
+ EOF
300
+
301
+ opt.on('-s', '--status',
302
+ 'Check if an app is running.') do
303
+ options['return'] = :status
304
+ end
305
+
306
+ opt.on('-d', '--details',
307
+ 'Get details about the deployed apps.') do
308
+ options['return'] = :details
309
+ end
310
+
311
+
312
+ vals = [:enable, :disable, :remove]
313
+ desc = "Set or get the healthcheck status. (#{vals.join(", ")})"
314
+
315
+ opt.on('-h', '--health [STATUS]', vals, desc) do |status|
316
+ options['health'] = status.to_sym if status
317
+ options['return'] = :health
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,62 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Runs the restart script of all specified sunshine apps.
5
+ #
6
+ # Usage: sunshine restart app_name [more names...] [options]
7
+ #
8
+ # Arguments:
9
+ # app_name Name of the application to restart.
10
+ #
11
+ # Options:
12
+ # -f, --format FORMAT Set the output format (txt, yml, json)
13
+ # -u, --user USER User to use for remote login. Use with -r.
14
+ # -r, --remote svr1,svr2 Run on one or more remote servers.
15
+ # -v, --verbose Run in verbose mode.
16
+
17
+ class RestartCommand < ListCommand
18
+
19
+ ##
20
+ # Takes an array and a hash, runs the command and returns:
21
+ # true: success
22
+ # false: failed
23
+ # exitcode:
24
+ # code == 0: success
25
+ # code != 0: failed
26
+ # and optionally an accompanying message.
27
+
28
+ def self.exec names, config
29
+ output = exec_each_server config do |shell|
30
+ new(shell).restart(names)
31
+ end
32
+
33
+ return output
34
+ end
35
+
36
+
37
+ ##
38
+ # Restart specified apps.
39
+
40
+ def restart app_names
41
+ status_after_command :restart, app_names
42
+ end
43
+
44
+
45
+ ##
46
+ # Parses the argv passed to the command
47
+
48
+ def self.parse_args argv
49
+ parse_remote_args(argv) do |opt, options|
50
+ opt.banner = <<-EOF
51
+
52
+ Usage: #{opt.program_name} restart app_name [more names...] [options]
53
+
54
+ Arguments:
55
+ app_name Name of the application to restart.
56
+ EOF
57
+
58
+ end
59
+ end
60
+ end
61
+ end
62
+
@@ -0,0 +1,83 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Unregister a sunshine app.
5
+ #
6
+ # Usage: sunshine rm app_name [more names...] [options]
7
+ #
8
+ # Arguments:
9
+ # app_name Name of the application to remove.
10
+ #
11
+ # Options:
12
+ # -d, --delete Delete the app directory.
13
+ # -f, --format FORMAT Set the output format (txt, yml, json)
14
+ # -u, --user USER User to use for remote login. Use with -r.
15
+ # -r, --remote svr1,svr2 Run on one or more remote servers.
16
+ # -v, --verbose Run in verbose mode.
17
+
18
+ class RmCommand < ListCommand
19
+
20
+ ##
21
+ # Takes an array and a hash, runs the command and returns:
22
+ # true: success
23
+ # false: failed
24
+ # exitcode:
25
+ # code == 0: success
26
+ # code != 0: failed
27
+ # and optionally an accompanying message.
28
+
29
+
30
+ def self.exec names, config
31
+ delete_dir = config['delete_dir']
32
+
33
+ output = exec_each_server config do |shell|
34
+ server_command = new(shell)
35
+ results = server_command.remove(names, delete_dir)
36
+
37
+ self.save_list server_command.app_list, shell
38
+
39
+ results
40
+ end
41
+
42
+ return output
43
+ end
44
+
45
+
46
+ ##
47
+ # Remove a registered app on a given deploy server
48
+
49
+ def remove app_names, delete_dir=false
50
+ each_app(*app_names) do |server_app|
51
+ if delete_dir
52
+ server_app.stop rescue nil
53
+ server_app.shell.call "rm -rf #{server_app.root_path}"
54
+
55
+ server_app.crontab.delete!
56
+ end
57
+
58
+ @app_list.delete server_app.name
59
+ end
60
+ end
61
+
62
+
63
+ ##
64
+ # Parses the argv passed to the command
65
+
66
+ def self.parse_args argv
67
+ parse_remote_args(argv) do |opt, options|
68
+ opt.banner = <<-EOF
69
+
70
+ Usage: #{opt.program_name} rm app_name [more names...] [options]
71
+
72
+ Arguments:
73
+ app_name Name of the application to remove.
74
+ EOF
75
+
76
+ opt.on('-d', '--delete',
77
+ 'Delete the app directory.') do
78
+ options['delete_dir'] = true
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,151 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Run one or more sunshine scripts.
5
+ #
6
+ # Usage: sunshine run [run_file] [options]
7
+ #
8
+ # Arguments:
9
+ # run_file Load a script or app path. Defaults to ./Sunshine
10
+ #
11
+ # Options:
12
+ # -l, --level LEVEL Set trace level. Defaults to info.
13
+ # -e, --env DEPLOY_ENV Sets the deploy env. Defaults to development.
14
+ # -a, --auto Non-interactive - automate or fail.
15
+ # --no-trace Don't trace any output.
16
+
17
+ class RunCommand < DefaultCommand
18
+
19
+ ##
20
+ # Takes an array and a hash, runs the command and returns:
21
+ # true: success
22
+ # false: failed
23
+ # exitcode:
24
+ # code == 0: success
25
+ # code != 0: failed
26
+ # and optionally an accompanying message.
27
+
28
+ def self.exec run_files, config
29
+
30
+ run_files.each do |run_file|
31
+
32
+ run_file = run_file_from run_file
33
+
34
+ with_load_path File.dirname(run_file) do
35
+
36
+ puts "Running #{run_file}"
37
+
38
+ get_file_data run_file
39
+
40
+ require run_file
41
+ end
42
+ end
43
+
44
+ return true
45
+ end
46
+
47
+
48
+ ##
49
+ # Tries to infer what run file to used based on a given path:
50
+ # run_file_from "path/to/some/dir"
51
+ # #=> "path/to/some/dir/Sunshine"
52
+ # run_file_from nil
53
+ # #=> "Sunshine"
54
+ # run_file_from "path/to/run_script.rb"
55
+ # #=> "path/to/run_script.rb"
56
+
57
+ def self.run_file_from run_file
58
+ run_file = File.join(run_file, "Sunshine") if
59
+ run_file && File.directory?(run_file)
60
+
61
+ run_file ||= "Sunshine"
62
+
63
+ File.expand_path run_file
64
+ end
65
+
66
+
67
+ ##
68
+ # Adds a directory to the ruby load path and runs the passed block.
69
+ # Useful for scripts to be able to reference their own dirs.
70
+
71
+ def self.with_load_path path
72
+ path = File.expand_path path
73
+
74
+ # TODO: Find a better way to make file path accessible to App objects.
75
+ Sunshine.send :remove_const, "PATH" if defined?(Sunshine::PATH)
76
+ Sunshine.const_set "PATH", path
77
+
78
+ added = unless $:.include? path
79
+ $: << path && true
80
+ end
81
+
82
+ yield
83
+
84
+ Sunshine.send :remove_const, "PATH"
85
+
86
+ $:.delete path if added
87
+ end
88
+
89
+
90
+ ##
91
+ # Returns file data in a run file as a File IO object.
92
+
93
+ def self.get_file_data run_file
94
+ # TODO: Find a better way to make file data accessible to App objects.
95
+ Sunshine.send :remove_const, "DATA" if defined?(Sunshine::DATA)
96
+ data_marker = "__END__\n"
97
+ line = nil
98
+
99
+ Sunshine.const_set("DATA", File.open(run_file, 'r'))
100
+
101
+ until line == data_marker || Sunshine::DATA.eof?
102
+ line = Sunshine::DATA.gets
103
+ end
104
+ end
105
+
106
+
107
+ ##
108
+ # Parses the argv passed to the command
109
+
110
+ def self.parse_args argv
111
+ options = {'trace' => true}
112
+
113
+ opts = opt_parser(options) do |opt|
114
+ opt.banner = <<-EOF
115
+
116
+ Usage: #{opt.program_name} run [run_file] [options]
117
+
118
+ Arguments:
119
+ run_file Load a script or app path. Defaults to ./Sunshine
120
+ EOF
121
+
122
+ opt.separator nil
123
+ opt.separator "Options:"
124
+
125
+ opt.on('-l', '--level LEVEL',
126
+ 'Set trace level. Defaults to info.') do |value|
127
+ options['level'] = value
128
+ end
129
+
130
+ opt.on('-e', '--env DEPLOY_ENV',
131
+ 'Sets the deploy env. Defaults to development.') do |value|
132
+ options['deploy_env'] = value
133
+ end
134
+
135
+ opt.on('-a', '--auto',
136
+ 'Non-interactive - automate or fail.') do
137
+ options['auto'] = true
138
+ end
139
+
140
+ opt.on('--no-trace',
141
+ "Don't trace any output.") do
142
+ options['trace'] = false
143
+ end
144
+ end
145
+
146
+ opts.parse! argv
147
+
148
+ options
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,72 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # Runs the start script of all specified sunshine apps.
5
+ #
6
+ # Usage: sunshine start app_name [more names...] [options]
7
+ #
8
+ # Arguments:
9
+ # app_name Name of the application to start.
10
+ #
11
+ # Options:
12
+ # -F, --force Stop apps that are running, then start them.
13
+ # -f, --format FORMAT Set the output format (txt, yml, json)
14
+ # -u, --user USER User to use for remote login. Use with -r.
15
+ # -r, --remote svr1,svr2 Run on one or more remote servers.
16
+ # -v, --verbose Run in verbose mode.
17
+
18
+ class StartCommand < ListCommand
19
+
20
+ ##
21
+ # Takes an array and a hash, runs the command and returns:
22
+ # true: success
23
+ # false: failed
24
+ # exitcode:
25
+ # code == 0: success
26
+ # code != 0: failed
27
+ # and optionally an accompanying message.
28
+
29
+ def self.exec names, config
30
+ force = config['force']
31
+
32
+ output = exec_each_server config do |shell|
33
+ new(shell).start(names, force)
34
+ end
35
+
36
+ return output
37
+ end
38
+
39
+
40
+ ##
41
+ # Start specified apps.
42
+
43
+ def start app_names, force=false
44
+ status_after_command :start, app_names do |server_app|
45
+
46
+ server_app.stop if server_app.running? && force
47
+ end
48
+ end
49
+
50
+
51
+ ##
52
+ # Parses the argv passed to the command
53
+
54
+ def self.parse_args argv
55
+ parse_remote_args(argv) do |opt, options|
56
+ opt.banner = <<-EOF
57
+
58
+ Usage: #{opt.program_name} start app_name [more names...] [options]
59
+
60
+ Arguments:
61
+ app_name Name of the application to start.
62
+ EOF
63
+
64
+ opt.on('-F', '--force',
65
+ 'Stop apps that are running before starting them again.') do
66
+ options['force'] = true
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+