etsy-deployinator 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +368 -0
- data/Rakefile +16 -0
- data/bin/deployinator-tailer.rb +78 -0
- data/deployinator.gemspec +31 -0
- data/lib/deployinator.rb +114 -0
- data/lib/deployinator/app.rb +204 -0
- data/lib/deployinator/base.rb +58 -0
- data/lib/deployinator/config.rb +7 -0
- data/lib/deployinator/controller.rb +147 -0
- data/lib/deployinator/helpers.rb +610 -0
- data/lib/deployinator/helpers/deploy.rb +68 -0
- data/lib/deployinator/helpers/dsh.rb +42 -0
- data/lib/deployinator/helpers/git.rb +348 -0
- data/lib/deployinator/helpers/plugin.rb +50 -0
- data/lib/deployinator/helpers/stack-tail.rb +32 -0
- data/lib/deployinator/helpers/version.rb +62 -0
- data/lib/deployinator/helpers/view.rb +67 -0
- data/lib/deployinator/logging.rb +16 -0
- data/lib/deployinator/plugin.rb +7 -0
- data/lib/deployinator/stack-tail.rb +34 -0
- data/lib/deployinator/static/css/diff_style.css +283 -0
- data/lib/deployinator/static/css/highlight.css +235 -0
- data/lib/deployinator/static/css/style.css +1223 -0
- data/lib/deployinator/static/js/flot/jquery.flot.min.js +1 -0
- data/lib/deployinator/static/js/flot/jquery.flot.selection.js +299 -0
- data/lib/deployinator/static/js/jquery-1.8.3.min.js +2 -0
- data/lib/deployinator/static/js/jquery-ui-1.8.24.min.js +5 -0
- data/lib/deployinator/static/js/jquery.timed_bar.js +36 -0
- data/lib/deployinator/tasks/initialize.rake +84 -0
- data/lib/deployinator/tasks/tests.rake +22 -0
- data/lib/deployinator/templates/deploys_status.mustache +42 -0
- data/lib/deployinator/templates/generic_single_push.mustache +64 -0
- data/lib/deployinator/templates/index.mustache +12 -0
- data/lib/deployinator/templates/layout.mustache +604 -0
- data/lib/deployinator/templates/log.mustache +24 -0
- data/lib/deployinator/templates/log_table.mustache +90 -0
- data/lib/deployinator/templates/messageboxes.mustache +29 -0
- data/lib/deployinator/templates/run_logs.mustache +15 -0
- data/lib/deployinator/templates/scroll_control.mustache +8 -0
- data/lib/deployinator/templates/stream.mustache +13 -0
- data/lib/deployinator/version.rb +3 -0
- data/lib/deployinator/views/deploys_status.rb +22 -0
- data/lib/deployinator/views/index.rb +14 -0
- data/lib/deployinator/views/layout.rb +48 -0
- data/lib/deployinator/views/log.rb +12 -0
- data/lib/deployinator/views/log_table.rb +35 -0
- data/lib/deployinator/views/run_logs.rb +32 -0
- data/templates/app.rb.erb +7 -0
- data/templates/config.ru.erb +10 -0
- data/templates/helper.rb.erb +15 -0
- data/templates/stack.rb.erb +17 -0
- data/templates/template.mustache +1 -0
- data/templates/view.rb.erb +7 -0
- data/test/unit/helpers_dsh_test.rb +40 -0
- data/test/unit/helpers_test.rb +77 -0
- data/test/unit/version_test.rb +104 -0
- metadata +245 -0
@@ -0,0 +1,610 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'timeout'
|
3
|
+
require 'deployinator/helpers/version'
|
4
|
+
require 'deployinator/helpers/plugin'
|
5
|
+
|
6
|
+
module Deployinator
|
7
|
+
module Helpers
|
8
|
+
include Deployinator::Helpers::VersionHelpers,
|
9
|
+
Deployinator::Helpers::PluginHelpers
|
10
|
+
|
11
|
+
RUN_LOG_PATH = "run_logs/"
|
12
|
+
|
13
|
+
def dev_context?
|
14
|
+
Deployinator.app_context['context'] == 'dev'
|
15
|
+
end
|
16
|
+
|
17
|
+
def not_dev?
|
18
|
+
Deployinator.app_context['context'] != 'dev'
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_log_path
|
22
|
+
RUN_LOG_PATH
|
23
|
+
end
|
24
|
+
|
25
|
+
def init(env)
|
26
|
+
@username = 'nobody'
|
27
|
+
@groups = ['nogroup']
|
28
|
+
@local = false
|
29
|
+
@host = env['HTTP_HOST']
|
30
|
+
auth_info = raise_event(:auth, {:env => env, :request => request})
|
31
|
+
if !auth_info.nil?
|
32
|
+
raise "You must login." unless auth_info[:authorized]
|
33
|
+
@username = auth_info[:username]
|
34
|
+
@groups = auth_info[:groups]
|
35
|
+
@host = auth_info[:host]
|
36
|
+
@local = auth_info[:local]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def write_file(str, file)
|
41
|
+
File.open("#{RUN_LOG_PATH}#{file}", "a:UTF-8") do |f|
|
42
|
+
f.print str.force_encoding("UTF-8")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Creates a current-stackname symlink for each deploy for easier tailing
|
47
|
+
#
|
48
|
+
# filename - String of the current run log filename
|
49
|
+
# stack - String containing the stack for this deploy
|
50
|
+
def link_stack_logfile(filename, stack)
|
51
|
+
run_cmd %Q{ln -nfs #{Deployinator.root_dir}/#{run_log_path}#{filename} #{Deployinator.root_dir}/#{run_log_path}current-#{stack}}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Moves current-stackname symlink so tailer won't accidentally pick up on last push
|
55
|
+
# race condition
|
56
|
+
#
|
57
|
+
# stack - String containing the stack for this deploy
|
58
|
+
def move_stack_logfile(stack)
|
59
|
+
run_cmd %Q{mv #{Deployinator.root_dir}/#{run_log_path}current-#{stack} #{Deployinator.root_dir}/#{run_log_path}last-#{stack}}
|
60
|
+
end
|
61
|
+
|
62
|
+
# output to a file, and the streaming output handler
|
63
|
+
# Public: helper function to write a message to the logfile and have it
|
64
|
+
# streamed in the webfrontend also. The frontend is HTML markuped so you
|
65
|
+
# can use HTML in the log message and it will be rendered with the given
|
66
|
+
# CSS of the site. Some classes can be used per default in Deployinator to
|
67
|
+
# show the output also in an error or info notification box. These are then
|
68
|
+
# displayed in a box above the logging output.
|
69
|
+
#
|
70
|
+
# output - String to be logged and shown in the output
|
71
|
+
#
|
72
|
+
# Examples:
|
73
|
+
# log_and_stream(<div class="stderror"> ERROR! </div>)
|
74
|
+
# log_and_stream(<div class="info_msg"> INFO! </div>)
|
75
|
+
#
|
76
|
+
# Returns nothing
|
77
|
+
def log_and_stream(output)
|
78
|
+
write_file output, @filename if @filename
|
79
|
+
return @block.call(output) unless @block.nil?
|
80
|
+
""
|
81
|
+
end
|
82
|
+
|
83
|
+
# Run external command with timing information
|
84
|
+
# streams and logs the output of the command as well
|
85
|
+
# If the command fails, it is retried some number of times
|
86
|
+
# This defaults to 5, but can be specified with the num_retries parameter
|
87
|
+
# If all the retries fail, an exception is thrown
|
88
|
+
# Between retries it will sleep for a given period, defaulting to 2 seconds
|
89
|
+
def run_cmd_with_retries(cmd, num_retries=5, sleep_seconds=2, timing_metric=nil)
|
90
|
+
for i in 1..num_retries
|
91
|
+
if i == num_retries then
|
92
|
+
result = run_cmd(cmd, timing_metric)
|
93
|
+
else
|
94
|
+
result = run_cmd(cmd, timing_metric, false)
|
95
|
+
end
|
96
|
+
if result[:exit_code] == 0
|
97
|
+
return result
|
98
|
+
else
|
99
|
+
retries_remaining = num_retries - i
|
100
|
+
unless i == num_retries
|
101
|
+
log_and_stream("`#{cmd}` failed, will retry #{retries_remaining} more times<br>")
|
102
|
+
sleep sleep_seconds
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
raise "Unable to execute `#{cmd}` after retrying #{num_retries} times"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Run external command with timing information
|
111
|
+
# streams and logs the output of the command as well
|
112
|
+
# does not (currently) check exit status codes
|
113
|
+
def run_cmd(cmd, timing_metric=nil, log_errors=true)
|
114
|
+
ret = ""
|
115
|
+
exit_code = 0
|
116
|
+
start = Time.now.to_i
|
117
|
+
timestamp = Time.now.to_s
|
118
|
+
plugin_state = {
|
119
|
+
:cmd => cmd,
|
120
|
+
:timing_metric => timing_metric,
|
121
|
+
:start_time => start,
|
122
|
+
:log_errors => log_errors
|
123
|
+
}
|
124
|
+
raise_event(:run_command_start, plugin_state)
|
125
|
+
log_and_stream "<div class='command'><h4>#{timestamp}: Running #{cmd}</h4>\n<p class='output'>"
|
126
|
+
time = Benchmark.measure do
|
127
|
+
Open3.popen3(cmd) do |inn, out, err, wait_thr|
|
128
|
+
output = ""
|
129
|
+
until out.eof?
|
130
|
+
# raise "Timeout" if output.empty? && Time.now.to_i - start > 300
|
131
|
+
chr = out.read(1)
|
132
|
+
output << chr
|
133
|
+
ret << chr
|
134
|
+
if chr == "\n" || chr == "\r"
|
135
|
+
log_and_stream output + "<br>"
|
136
|
+
output = ""
|
137
|
+
end
|
138
|
+
end
|
139
|
+
error_message = nil
|
140
|
+
log_and_stream(output) unless output.empty?
|
141
|
+
|
142
|
+
error_message = err.read unless err.eof?
|
143
|
+
if (log_errors) then
|
144
|
+
log_and_stream("<span class='stderr'>STDERR: #{error_message}</span><br>") unless error_message.nil?
|
145
|
+
else
|
146
|
+
log_and_stream("STDERR:" + error_message + "<br>") unless error_message.nil?
|
147
|
+
end
|
148
|
+
|
149
|
+
unless error_message.nil? then
|
150
|
+
plugin_state[:error_message] = error_message
|
151
|
+
raise_event(:run_command_error, plugin_state)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Log non-zero exits
|
155
|
+
if wait_thr.value.exitstatus != 0 then
|
156
|
+
log_and_stream("<span class='stderr'>DANGER! #{cmd} had an exit value of: #{wait_thr.value.exitstatus}</span><br>")
|
157
|
+
exit_code = wait_thr.value.exitstatus
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
log_and_stream "</p>"
|
162
|
+
log_and_stream "<h5>Time: #{time}</h5></div>"
|
163
|
+
plugin_state[:exit_code] = exit_code
|
164
|
+
plugin_state[:stdout] = ret
|
165
|
+
plugin_state[:time] = time.real
|
166
|
+
raise_event(:run_command_end, plugin_state)
|
167
|
+
return { :stdout => ret, :exit_code => exit_code }
|
168
|
+
end
|
169
|
+
|
170
|
+
def nicify_env(env)
|
171
|
+
env = "production" if env == "PROD"
|
172
|
+
env.downcase
|
173
|
+
end
|
174
|
+
|
175
|
+
def http_host
|
176
|
+
@host
|
177
|
+
end
|
178
|
+
|
179
|
+
def stack
|
180
|
+
@stack
|
181
|
+
end
|
182
|
+
|
183
|
+
%w[dev qa stage princess production].each do |env|
|
184
|
+
define_method "#{env}_version" do
|
185
|
+
version = self.send("#{stack}_#{env}_version")
|
186
|
+
if @disabled_override == true
|
187
|
+
@disabled = false
|
188
|
+
else
|
189
|
+
@disabled = !version || version.empty?
|
190
|
+
end
|
191
|
+
version
|
192
|
+
end
|
193
|
+
|
194
|
+
define_method "#{env}_build" do
|
195
|
+
get_build(self.send("#{env}_version"))
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def diff(r1, r2, stack="web", time=null)
|
200
|
+
if (!time)
|
201
|
+
time = Time.now.to_i
|
202
|
+
end
|
203
|
+
redirect "/diff/#{stack}/#{r1}/#{r2}/github?time=#{time}"
|
204
|
+
return
|
205
|
+
end
|
206
|
+
|
207
|
+
def send_email(options)
|
208
|
+
Pony.mail(options)
|
209
|
+
end
|
210
|
+
|
211
|
+
def get_log
|
212
|
+
log_entries.collect do |line|
|
213
|
+
"[" + line.split("|").join("] [") + "]"
|
214
|
+
end.join("<br>")
|
215
|
+
end
|
216
|
+
|
217
|
+
# Public: log a given message to the log_path file. The method calls
|
218
|
+
# log_string_to_file for lower level logging functionality.
|
219
|
+
#
|
220
|
+
# env - String which represents the environment the log was produced in
|
221
|
+
# who - String which represents the active user
|
222
|
+
# msg - String representing the actual log message
|
223
|
+
# stack - String representing the current deploy stack
|
224
|
+
#
|
225
|
+
# Returns the return code of log_string_to_file
|
226
|
+
def log(env, who, msg, stack)
|
227
|
+
s = stack
|
228
|
+
log_string_to_file("#{now}|#{env}|#{clean(who)}|#{clean(msg)}|#{s}|#{@filename}", Deployinator.log_path)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Public: wrapper method around appending stdout to a logfile.
|
232
|
+
#
|
233
|
+
# string - String representing the log message
|
234
|
+
# path - String representing the path to the logfile
|
235
|
+
#
|
236
|
+
# Returns true if echo exited with 0, false for non-zero exit and nil if
|
237
|
+
# the call fails
|
238
|
+
def log_string_to_file(string, path)
|
239
|
+
cmd = %Q{echo "#{string}" >> #{path}}
|
240
|
+
system(cmd)
|
241
|
+
end
|
242
|
+
|
243
|
+
def clean(msg)
|
244
|
+
(msg || "").gsub("|", "/").gsub('"', """).gsub("'", "'")
|
245
|
+
end
|
246
|
+
|
247
|
+
def nice_time
|
248
|
+
"%Y-%m-%d %H:%M:%S"
|
249
|
+
end
|
250
|
+
|
251
|
+
def now
|
252
|
+
Time.now.gmtime.strftime(nice_time)
|
253
|
+
end
|
254
|
+
|
255
|
+
def hyperlink(msg)
|
256
|
+
(msg || "").gsub(/([A-Z]{2,10}-[0-9]{2,})/) do |issue|
|
257
|
+
issue_url = Deployinator.issue_tracker.call(issue)
|
258
|
+
"<a href='#{issue_url}' target='_blank'>#{issue}</a>"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def environments
|
263
|
+
custom_env = "#{stack}_environments"
|
264
|
+
envs = send(custom_env) if respond_to?(custom_env.to_sym)
|
265
|
+
envs ||=
|
266
|
+
[{
|
267
|
+
:name => "production",
|
268
|
+
:title => "Deploy #{stack} production",
|
269
|
+
:method => "production",
|
270
|
+
:current_version => proc{send(:"#{stack}_production_version")},
|
271
|
+
:current_build => proc{get_build(send(:"#{stack}_production_version"))},
|
272
|
+
:next_build => proc{send(:head_build)}
|
273
|
+
}]
|
274
|
+
|
275
|
+
# Support simplified symbol for methods
|
276
|
+
envs.map! do |env|
|
277
|
+
new_env = env
|
278
|
+
new_env[:current_version] = proc{send(env[:current_version])} if env[:current_version].is_a? Symbol
|
279
|
+
new_env[:current_build] = proc{send(env[:current_build])} if env[:current_build].is_a? Symbol
|
280
|
+
new_env[:next_build] = proc{send(env[:next_build])} if env[:next_build].is_a? Symbol
|
281
|
+
new_env
|
282
|
+
end
|
283
|
+
|
284
|
+
envs.each_with_index { |env, i| env[:number] = "%02d." % (i + 1); env[:not_last] = (i < envs.size - 1) }
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
# Public: fetch the run_logs in 'run_logs/' based on sevaral parameters
|
289
|
+
#
|
290
|
+
# Parameters:
|
291
|
+
# opts
|
292
|
+
# :offset
|
293
|
+
# :limit
|
294
|
+
# :filename - <stack>-<method>
|
295
|
+
#
|
296
|
+
# Returns an array of hashes with name and time keys
|
297
|
+
def get_run_logs(opts={})
|
298
|
+
offset = opts[:offset] || 0
|
299
|
+
limit = opts[:limit] || -1
|
300
|
+
filename = opts[:filename] || ""
|
301
|
+
glob = Deployinator::Helpers::RUN_LOG_PATH + "*.html"
|
302
|
+
files = Dir.glob(glob)
|
303
|
+
|
304
|
+
# filter for config files
|
305
|
+
files.select! {|file| file.match(/^((?!web[-_]config_diff.html).)*$/) && file.match(/html/)}
|
306
|
+
|
307
|
+
# filter for princess or production run_logs
|
308
|
+
files.select! {|file| file.match(/#{filename}/)}
|
309
|
+
|
310
|
+
# map files to hash with name and time keys
|
311
|
+
files.map! do |file|
|
312
|
+
{ :name => File.basename(file), :time => Time.at(file[/(\d{8,})/].to_i) }
|
313
|
+
end
|
314
|
+
|
315
|
+
# sort files chronologically,
|
316
|
+
files.sort_by! {|file| file[:time]}.reverse!
|
317
|
+
|
318
|
+
# select files within an index range
|
319
|
+
files[offset...offset +limit]
|
320
|
+
|
321
|
+
end
|
322
|
+
|
323
|
+
# Public: strips all of the whitespace from a string. If the string only whitespace, return nil.
|
324
|
+
#
|
325
|
+
# s - the string to strip whitespace from
|
326
|
+
#
|
327
|
+
# Example
|
328
|
+
# if strip_ws_to_nil(hostname).nil?
|
329
|
+
# puts "blank hostname is not valid!"
|
330
|
+
# end
|
331
|
+
#
|
332
|
+
# Returns A whitespace-free string or nil.
|
333
|
+
def strip_ws_to_nil(s)
|
334
|
+
if s.nil?
|
335
|
+
nil
|
336
|
+
else
|
337
|
+
s = s.gsub(/\s+/, "")
|
338
|
+
if s == ''
|
339
|
+
nil
|
340
|
+
else
|
341
|
+
s
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Public: gets the contes from a cache file if it hasn't expired
|
347
|
+
#
|
348
|
+
# Paramaters:
|
349
|
+
# cache_file: path to a cache file
|
350
|
+
# cache_ttl : how long in seconds the cached content is good for
|
351
|
+
# A negative number will indicate you don't care how old the cache
|
352
|
+
# file is.
|
353
|
+
#
|
354
|
+
# Returns: cached content or false if expired or cache file doesn't exist.
|
355
|
+
def get_from_cache(cache_file, cache_ttl=5)
|
356
|
+
if File.exists?(cache_file)
|
357
|
+
now = Time.now
|
358
|
+
file_mtime = File.mtime(cache_file)
|
359
|
+
file_age = now - file_mtime
|
360
|
+
if ((cache_ttl < 0) || (file_age <= cache_ttl))
|
361
|
+
file = File.open(cache_file, "r:UTF-8")
|
362
|
+
return file.read
|
363
|
+
else
|
364
|
+
# Return False if the cache is old
|
365
|
+
return false
|
366
|
+
end
|
367
|
+
else
|
368
|
+
# Return False if the cache file doesn't exist
|
369
|
+
return false
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def lock_pushes(stack, who, method)
|
374
|
+
log_and_stream("LOCKING #{stack}")
|
375
|
+
if lock_info = push_lock_info(stack)
|
376
|
+
return log_and_stream("Pushes locked by #{lock_info[:who]} - #{lock_info[:method]}")
|
377
|
+
end
|
378
|
+
|
379
|
+
dt = Time.now.strftime("%m/%d/%Y %H:%M")
|
380
|
+
log_string_to_file("#{who}|#{method}|#{dt}", push_lock_path(stack))
|
381
|
+
end
|
382
|
+
|
383
|
+
def unlock_pushes(stack)
|
384
|
+
system(%Q{rm #{push_lock_path(stack)}})
|
385
|
+
end
|
386
|
+
|
387
|
+
def push_lock_info(stack)
|
388
|
+
d = `test -f #{push_lock_path(stack)} && cat #{push_lock_path(stack)}`.chomp
|
389
|
+
d.empty? ? nil : Hash[*[:who, :method, :lock_time].zip(d.split("|")).flatten]
|
390
|
+
end
|
391
|
+
|
392
|
+
def pushes_locked?(stack)
|
393
|
+
push_lock_info(stack)
|
394
|
+
end
|
395
|
+
|
396
|
+
def push_lock_path(stack)
|
397
|
+
"#{Deployinator.root(["log"])}/#{stack}-push-lock"
|
398
|
+
end
|
399
|
+
|
400
|
+
# Public: Outputs stack data for use in templating
|
401
|
+
# the stack selection box in the header.
|
402
|
+
#
|
403
|
+
# Returns an array of hashes with the fields "stack" and "current"
|
404
|
+
# where "current" is true for the currently selected stack.
|
405
|
+
def get_stack_select
|
406
|
+
stacks = Deployinator.get_stacks
|
407
|
+
output = Array.new
|
408
|
+
stacks.each do |s|
|
409
|
+
current = stack == s
|
410
|
+
output << { "stack" => s, "current" => current }
|
411
|
+
end
|
412
|
+
output
|
413
|
+
end
|
414
|
+
|
415
|
+
# Public: given a run logs filename, return a full URL to the runlg
|
416
|
+
#
|
417
|
+
# Params:
|
418
|
+
# filename - string of the filename
|
419
|
+
#
|
420
|
+
# Returns a string URL where that runlog can be viewed
|
421
|
+
def run_log_url(filename)
|
422
|
+
"http://#{Deployinator.hostname}/run_logs/view/#{filename}"
|
423
|
+
end
|
424
|
+
|
425
|
+
# Public: wrap a block into a timeout
|
426
|
+
#
|
427
|
+
# seconds - timeout in seconds
|
428
|
+
# description - optional description for logging (default:"")
|
429
|
+
# &block - block to call
|
430
|
+
#
|
431
|
+
# Example
|
432
|
+
# with_timeout(20){system("curl -s http://google.com")}
|
433
|
+
# with_timeout 30 do; system("curl -s http://google.com"); end
|
434
|
+
#
|
435
|
+
# Returns nothing
|
436
|
+
def with_timeout(seconds, description=nil, &block)
|
437
|
+
begin
|
438
|
+
Timeout.timeout(seconds) do
|
439
|
+
yield
|
440
|
+
end
|
441
|
+
rescue Timeout::Error => e
|
442
|
+
info = "#{Time.now}: Timeout: #{e}"
|
443
|
+
info += " for #{description}" unless description.nil?
|
444
|
+
# log and stream if log filename is not undefined
|
445
|
+
if (/undefined/ =~ @filename).nil?
|
446
|
+
log_and_stream "<div class=\"stderr\">#{info}</div>"
|
447
|
+
end
|
448
|
+
state = {
|
449
|
+
:seconds => seconds,
|
450
|
+
:info => info
|
451
|
+
}
|
452
|
+
raise_event(:timeout, state)
|
453
|
+
""
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def can_remove_stack_lock?
|
458
|
+
unless @groups.nil? then
|
459
|
+
Deployinator.admin_groups.each { |cg| return true if @groups.include?(cg) }
|
460
|
+
end
|
461
|
+
|
462
|
+
# get the lock info to see if the user is the locker
|
463
|
+
info = push_lock_info(@stack) || {}
|
464
|
+
return true if info.empty?
|
465
|
+
if info[:who] == @username
|
466
|
+
return true
|
467
|
+
end
|
468
|
+
|
469
|
+
return false
|
470
|
+
end
|
471
|
+
|
472
|
+
def announce(announcement, options = {})
|
473
|
+
raise_event(:announce, {:announcement => announcement, :options => options})
|
474
|
+
|
475
|
+
if options[:send_email] && options[:send_email] == true
|
476
|
+
stack = options[:stack]
|
477
|
+
|
478
|
+
send_email({
|
479
|
+
:subject => "#{stack} deployed #{options[:env]} by #{@username}",
|
480
|
+
:html_body => announcement
|
481
|
+
})
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def diff_url(stack, old_build, new_build)
|
486
|
+
raise_event(:diff, {:stack => stack, :old_build => old_build, :new_build => new_build})
|
487
|
+
end
|
488
|
+
|
489
|
+
def log_and_shout(options={})
|
490
|
+
options[:stack] ||= @stack
|
491
|
+
|
492
|
+
raise "Must have stack" unless options[:stack]
|
493
|
+
|
494
|
+
options[:env] ||= "PROD"
|
495
|
+
options[:nice_env] = nicify_env(options[:env])
|
496
|
+
options[:user] ||= @username
|
497
|
+
options[:start] = @start_time unless options[:start] || ! @start_time
|
498
|
+
|
499
|
+
if (options[:start])
|
500
|
+
options[:end] = Time.now.to_i unless options.key?(:end)
|
501
|
+
options[:duration] = options[:end] - options[:start]
|
502
|
+
|
503
|
+
log_and_stream "Ended at #{options[:end]}<br>Took: #{options[:duration]} seconds<br>"
|
504
|
+
timing_log options[:duration], options[:nice_env], options[:stack]
|
505
|
+
end
|
506
|
+
|
507
|
+
if (options[:old_build] && options[:build])
|
508
|
+
log_str = "#{options[:stack]} #{options[:nice_env]} deploy: old #{options[:old_build]}, new: #{options[:build]}"
|
509
|
+
log options[:env], options[:user], log_str, options[:stack]
|
510
|
+
d_url = diff_url(options[:stack], options[:old_build], options[:build])
|
511
|
+
end
|
512
|
+
|
513
|
+
if (options[:old_build] && options[:build] && (options[:irc_channels] || options[:send_email]))
|
514
|
+
announcement = "#{options[:stack]} #{options[:env]} deployed by #{options[:user]}"
|
515
|
+
announcement << " build: #{options[:build]} took: #{options[:duration]} seconds "
|
516
|
+
announcement << "diff: #{d_url}" if d_url
|
517
|
+
announce announcement, options
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
def timing_log(duration, type, stack, timestamp=nil)
|
522
|
+
if (timestamp == nil) then
|
523
|
+
timestamp = Time.now.to_i
|
524
|
+
end
|
525
|
+
|
526
|
+
current = now()
|
527
|
+
log_string_to_file("#{current}|#{type}|#{stack}|#{duration}", Deployinator.timing_log_path)
|
528
|
+
raise_event(:timing_log, {:duration => duration, :current => current, :type => type, :timestamp => timestamp})
|
529
|
+
end
|
530
|
+
|
531
|
+
def average_duration(type, stack)
|
532
|
+
log = `grep "#{type}|#{stack}" #{Deployinator.timing_log_path} | tac | head -5`
|
533
|
+
timings = log.split("\n").collect { |line| line.split("|").last.to_f }
|
534
|
+
avg_time = (timings.empty?) ? 30 : timings.inject(0) {|a,v| a+v} / timings.size
|
535
|
+
puts "avg time for #{stack}/#{type}: #{avg_time}"
|
536
|
+
avg_time
|
537
|
+
end
|
538
|
+
|
539
|
+
def log_entries(options = {})
|
540
|
+
stacks = []
|
541
|
+
stacks << "LOG MESSAGE" unless (options[:no_limit] || options[:no_global])
|
542
|
+
|
543
|
+
stacks << options[:stack] if options[:stack]
|
544
|
+
|
545
|
+
env = options[:env] ? "\\|#{options[:env].upcase}\\|" : ""
|
546
|
+
limit = (options[:no_limit] && options[:no_limit] == true) ? nil : (options[:limit] || 40)
|
547
|
+
|
548
|
+
# stack should be the last part of the log line from the last pipe to the end
|
549
|
+
# modified this to take into account the run_log entry at the end
|
550
|
+
unless stacks.empty? && env.empty?
|
551
|
+
grep = "| egrep '#{env}.*\\|\(#{stacks.join("|")}\)(|(\\||$))?'"
|
552
|
+
end
|
553
|
+
|
554
|
+
# extra grep does another filter to the line, needed to get CONFIG PRODUCTION
|
555
|
+
if defined? options[:extragrep]
|
556
|
+
extragrep = "| egrep -i '#{options[:extragrep]}' "
|
557
|
+
end
|
558
|
+
|
559
|
+
if options[:page]
|
560
|
+
num_per_page = 40
|
561
|
+
limit = "| head -#{num_per_page * options[:page].to_i} | tail -#{num_per_page}"
|
562
|
+
else
|
563
|
+
limit = "| head -#{limit}" if limit
|
564
|
+
end
|
565
|
+
|
566
|
+
log = `tac #{Deployinator.log_path} #{grep} #{extragrep} #{limit}`
|
567
|
+
log.split("\n")
|
568
|
+
end
|
569
|
+
|
570
|
+
# Public: check if the deploy host is up or not
|
571
|
+
# show a little slug in the header with the deploy host name and status
|
572
|
+
def get_deploy_target_status
|
573
|
+
status = %x{ssh -o ConnectTimeout=5 #{Deployinator.deploy_host} uptime | awk '{print $2}' }.rstrip
|
574
|
+
if status != 'up'
|
575
|
+
status = "<b>DOWN!</b>"
|
576
|
+
end
|
577
|
+
"#{Deployinator.deploy_host} is #{status}"
|
578
|
+
end
|
579
|
+
|
580
|
+
def deploy_host?
|
581
|
+
! Deployinator.deploy_host.nil?
|
582
|
+
end
|
583
|
+
|
584
|
+
def head_build
|
585
|
+
meth = "#{stack}_head_build"
|
586
|
+
if self.respond_to?(meth)
|
587
|
+
self.send(meth)
|
588
|
+
else
|
589
|
+
if git_info_for_stack.key?(stack.to_sym)
|
590
|
+
rev = get_git_head_rev(stack)
|
591
|
+
puts rev
|
592
|
+
return rev
|
593
|
+
else
|
594
|
+
puts "ERROR: add your stack in git_info_for_stack"
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
def log_error(msg, e = nil)
|
600
|
+
log_msg = e.nil? ? msg : "#{msg} (#{e.message})"
|
601
|
+
log_and_stream "<div class=\"stderr\">#{log_msg}</div>"
|
602
|
+
if !e.nil?
|
603
|
+
log_and_stream e.backtrace.inspect
|
604
|
+
end
|
605
|
+
# This is so we have something in the log if/when this fails
|
606
|
+
puts log_msg
|
607
|
+
end
|
608
|
+
|
609
|
+
end
|
610
|
+
end
|