omnibus-ctl 0.3.5 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/omnibus-ctl.rb CHANGED
@@ -1,5 +1,4 @@
1
- #
2
- # Copyright:: Copyright (c) 2012 Opscode, Inc.
1
+ # Copyright (c) 2012-2015 Chef Software, Inc.
3
2
  # License:: Apache License, Version 2.0
4
3
  #
5
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,127 +15,156 @@
16
15
  #
17
16
 
18
17
  require "omnibus-ctl/version"
19
- require 'json'
20
- require 'fileutils'
18
+ require "json" unless defined?(JSON)
19
+ require "fileutils" unless defined?(FileUtils)
20
+
21
+ # For license checks
22
+ require "io/console"
23
+ require "io/wait"
21
24
 
22
25
  module Omnibus
23
26
  class Ctl
24
27
 
25
- File::umask(022)
28
+ File.umask(022)
26
29
 
27
- SV_COMMAND_NAMES = %w[status up down once pause cont hup alarm interrupt quit
30
+ SV_COMMAND_NAMES = %w{status up down once pause cont hup alarm int quit
28
31
  term kill start stop restart shutdown force-stop
29
- force-reload force-restart force-shutdown check]
32
+ force-reload force-restart force-shutdown check usr1 usr2}.freeze
30
33
 
31
34
  attr_accessor :name, :display_name, :log_exclude, :base_path, :sv_path,
32
35
  :service_path, :etc_path, :data_path, :log_path, :command_map, :category_command_map,
33
36
  :fh_output, :kill_users, :verbose, :log_path_exclude
34
37
 
35
- def initialize(name, service_commands=true)
38
+ attr_reader :backup_dir, :exe_name
39
+
40
+ def initialize(name, merge_service_commands = true, disp_name = nil)
36
41
  @name = name
37
- @service_commands = service_commands
38
- @display_name = name
42
+ @service_commands = merge_service_commands
43
+ @display_name = disp_name || name
39
44
  @base_path = "/opt/#{name}"
40
45
  @sv_path = File.join(@base_path, "sv")
41
46
  @service_path = File.join(@base_path, "service")
42
47
  @log_path = "/var/log/#{name}"
43
48
  @data_path = "/var/opt/#{name}"
44
49
  @etc_path = "/etc/#{name}"
45
- @log_exclude = '(config|lock|@|gzip|tgz|gz)'
46
- @log_path_exclude = ['*/sasl/*']
50
+ @log_exclude = "(config|lock|@|bz2|gz|gzip|tbz2|tgz|txz|xz|zip)"
51
+ @log_path_exclude = ["*/sasl/*"]
47
52
  @fh_output = STDOUT
48
53
  @kill_users = []
49
54
  @verbose = false
55
+ @quiet = false
56
+ @exe_name = File.basename($0)
57
+ @force_exit = false
58
+ @global_pre_hooks = {}
59
+
60
+ # TODO(ssd) 2017-03-28: Set SVDIR explicitly. Once we fix a bug
61
+ # in our debian support, where we rely on system-installed
62
+ # runit, we can likely change this back to ENV.delete("SVDIR")
63
+ ENV["SVDIR"] = service_path
64
+
50
65
  # backwards compat command map that does not have categories
51
- @command_map = { }
66
+ @command_map = {}
52
67
 
53
68
  # categoired commands that we want by default
54
69
  @category_command_map = {
55
70
  "general" => {
56
71
  "show-config" => {
57
- :desc => "Show the configuration that would be generated by reconfigure.",
58
- :arity => 1
72
+ desc: "Show the configuration that would be generated by reconfigure.",
73
+ arity: 1,
59
74
  },
60
75
  "reconfigure" => {
61
- :desc => "Reconfigure the application.",
62
- :arity => 1
76
+ desc: "Reconfigure the application.",
77
+ arity: 2,
63
78
  },
64
79
  "cleanse" => {
65
- :desc => "Delete *all* #{display_name} data, and start from scratch.",
66
- :arity => 2
80
+ desc: "Delete *all* #{display_name} data, and start from scratch.",
81
+ arity: 2,
67
82
  },
68
83
  "uninstall" => {
69
- :arity => 1,
70
- :desc => "Kill all processes and uninstall the process supervisor (data will be preserved)."
84
+ arity: 1,
85
+ desc: "Kill all processes and uninstall the process supervisor (data will be preserved).",
71
86
  },
72
87
  "help" => {
73
- :arity => 1,
74
- :desc => "Print this help message."
75
- }
76
- }
88
+ arity: 1,
89
+ desc: "Print this help message.",
90
+ },
91
+ },
77
92
  }
78
93
  service_command_map = {
79
94
  "service-management" => {
80
95
  "service-list" => {
81
- :arity => 1,
82
- :desc => "List all the services (enabled services appear with a *.)"
96
+ arity: 1,
97
+ desc: "List all the services (enabled services appear with a *.)",
83
98
  },
84
99
  "status" => {
85
- :desc => "Show the status of all the services.",
86
- :arity => 2
100
+ desc: "Show the status of all the services.",
101
+ arity: 2,
87
102
  },
88
103
  "tail" => {
89
- :desc => "Watch the service logs of all enabled services.",
90
- :arity => 2
104
+ desc: "Watch the service logs of all enabled services.",
105
+ arity: 2,
91
106
  },
92
107
  "start" => {
93
- :desc => "Start services if they are down, and restart them if they stop.",
94
- :arity => 2
108
+ desc: "Start services if they are down, and restart them if they stop.",
109
+ arity: 2,
95
110
  },
96
111
  "stop" => {
97
- :desc => "Stop the services, and do not restart them.",
98
- :arity => 2
112
+ desc: "Stop the services, and do not restart them.",
113
+ arity: 2,
99
114
  },
100
115
  "restart" => {
101
- :desc => "Stop the services if they are running, then start them again.",
102
- :arity => 2
116
+ desc: "Stop the services if they are running, then start them again.",
117
+ arity: 2,
103
118
  },
104
119
  "once" => {
105
- :desc => "Start the services if they are down. Do not restart them if they stop.",
106
- :arity => 2
120
+ desc: "Start the services if they are down. Do not restart them if they stop.",
121
+ arity: 2,
107
122
  },
108
123
  "hup" => {
109
- :desc => "Send the services a HUP.",
110
- :arity => 2
124
+ desc: "Send the services a HUP.",
125
+ arity: 2,
111
126
  },
112
127
  "term" => {
113
- :desc => "Send the services a TERM.",
114
- :arity => 2
128
+ desc: "Send the services a TERM.",
129
+ arity: 2,
115
130
  },
116
131
  "int" => {
117
- :desc => "Send the services an INT.",
118
- :arity => 2
132
+ desc: "Send the services an INT.",
133
+ arity: 2,
119
134
  },
120
135
  "kill" => {
121
- :desc => "Send the services a KILL.",
122
- :arity => 2
136
+ desc: "Send the services a KILL.",
137
+ arity: 2,
123
138
  },
124
139
  "graceful-kill" => {
125
- :desc => "Attempt a graceful stop, then SIGKILL the entire process group.",
126
- :arity => 2
127
- }
128
- }
140
+ desc: "Attempt a graceful stop, then SIGKILL the entire process group.",
141
+ arity: 2,
142
+ },
143
+ "usr1" => {
144
+ desc: "Send the services a USR1.",
145
+ arity: 2,
146
+ },
147
+ "usr2" => {
148
+ desc: "Send the services a USR2.",
149
+ arity: 2,
150
+ },
151
+ },
129
152
  }
130
153
  @category_command_map.merge!(service_command_map) if service_commands?
131
154
  end
132
155
 
156
+ def self.to_method_name(name)
157
+ name.gsub(/-/, "_").to_sym
158
+ end
159
+
160
+ def to_method_name(name)
161
+ Ctl.to_method_name(name)
162
+ end
163
+
133
164
  SV_COMMAND_NAMES.each do |sv_cmd|
134
- method_name = sv_cmd.gsub(/-/, "_")
135
- Omnibus::Ctl.class_eval <<-EOH
136
- def #{method_name}(*args)
165
+ define_method to_method_name(sv_cmd) do |*args|
137
166
  run_sv_command(*args)
138
167
  end
139
- EOH
140
168
  end
141
169
 
142
170
  # merges category_command_map and command_map,
@@ -160,29 +188,30 @@ module Omnibus
160
188
  end
161
189
 
162
190
  def load_file(filepath)
163
- eval(IO.read(filepath))
191
+ eval(IO.read(filepath), nil, filepath, 1) # rubocop: disable Security/Eval
164
192
  end
165
193
 
166
- def add_command(name, description, arity=1, &block)
167
- @command_map[name] = { :desc => description, :arity => arity }
168
- metaclass = class << self; self; end
169
- # Ruby does not like dashes in method names
170
- method_name = name.gsub(/-/, "_")
171
- metaclass.send(:define_method, method_name.to_sym) { |*args| block.call(*args) }
194
+ def add_command(name, description, arity = 1, &block)
195
+ @command_map[name] = { desc: description, arity: arity }
196
+ self.class.send(:define_method, to_method_name(name).to_sym) { |*args| block.call(*args) }
172
197
  end
173
198
 
174
- def add_command_under_category(name, category, description, arity=1, &block)
199
+ def add_command_under_category(name, category, description, arity = 1, &block)
175
200
  # add new category if it doesn't exist
176
- @category_command_map[category] = {} unless @category_command_map.has_key?(category)
177
- @category_command_map[category][name] = { :desc => description, :arity => arity }
178
- metaclass = class << self; self; end
179
- # Ruby does not like dashes in method names
180
- method_name = name.gsub(/-/, "_")
181
- metaclass.send(:define_method, method_name.to_sym) { |*args| block.call(*args) }
201
+ @category_command_map[category] ||= {}
202
+ @category_command_map[category][name] = { desc: description, arity: arity }
203
+ self.class.send(:define_method, to_method_name(name).to_sym) { |*args| block.call(*args) }
182
204
  end
183
205
 
184
- def exit!(error_code)
185
- exit error_code
206
+ def add_global_pre_hook(name, &block)
207
+ method_name = to_method_name("#{name}_global_pre_hook").to_sym
208
+ @global_pre_hooks[name] = method_name
209
+ self.class.send(:define_method, method_name, block)
210
+ end
211
+
212
+ def exit!(code)
213
+ @force_exit = true
214
+ code
186
215
  end
187
216
 
188
217
  def log(msg)
@@ -190,17 +219,17 @@ module Omnibus
190
219
  end
191
220
 
192
221
  def get_pgrp_from_pid(pid)
193
- ps=`which ps`.chomp
222
+ ps = `which ps`.chomp
194
223
  `#{ps} -p #{pid} -o pgrp=`.chomp
195
224
  end
196
225
 
197
226
  def get_pids_from_pgrp(pgrp)
198
- pgrep=`which pgrep`.chomp
227
+ pgrep = `which pgrep`.chomp
199
228
  `#{pgrep} -g #{pgrp}`.split(/\n/).join(" ")
200
229
  end
201
230
 
202
231
  def sigkill_pgrp(pgrp)
203
- pkill=`which pkill`.chomp
232
+ pkill = `which pkill`.chomp
204
233
  run_command("#{pkill} -9 -g #{pgrp}")
205
234
  end
206
235
 
@@ -218,40 +247,34 @@ module Omnibus
218
247
  exit! 0
219
248
  end
220
249
 
221
- def cleanup_procs_and_nuke(filestr)
222
- begin
223
- run_sv_command("stop")
224
- rescue SystemExit
225
- end
250
+ def cleanup_procs_and_nuke(filestr, calling_method = nil)
251
+ run_sv_command("stop")
226
252
 
227
- FileUtils.rm_f("/etc/init/#{name}-runsvdir.conf") if File.exists?("/etc/init/#{name}-runsvdir.conf")
228
- run_command("egrep -v '#{base_path}/embedded/bin/runsvdir-start' /etc/inittab > /etc/inittab.new && mv /etc/inittab.new /etc/inittab") if File.exists?("/etc/inittab")
253
+ FileUtils.rm_f("/etc/init/#{name}-runsvdir.conf") if File.exist?("/etc/init/#{name}-runsvdir.conf")
254
+ run_command("egrep -v '#{base_path}/embedded/bin/runsvdir-start' /etc/inittab > /etc/inittab.new && mv /etc/inittab.new /etc/inittab") if File.exist?("/etc/inittab")
229
255
  run_command("kill -1 1")
230
256
 
231
- backup_dir = Time.now.strftime("/root/#{name}-cleanse-%FT%R")
232
- FileUtils.mkdir_p("/root") unless File.exists?("/root")
257
+ @backup_dir = Time.now.strftime("/root/#{name}-cleanse-%FT%R")
258
+
259
+ FileUtils.mkdir_p("/root") unless File.exist?("/root")
233
260
  FileUtils.rm_rf(backup_dir)
234
- FileUtils.cp_r(etc_path, backup_dir) if File.exists?(etc_path)
261
+ FileUtils.cp_r(etc_path, backup_dir) if File.exist?(etc_path)
235
262
  run_command("rm -rf #{filestr}")
263
+ graceful_kill
236
264
 
237
- begin
238
- graceful_kill
239
- rescue SystemExit
240
- end
241
-
242
- run_command("pkill -HUP -u #{kill_users.join(',')}") if kill_users.length > 0
265
+ log "Terminating processes running under application users. This will take a few seconds."
266
+ run_command("pkill -HUP -u #{kill_users.join(",")}") if kill_users.length > 0
243
267
  run_command("pkill -HUP -f 'runsvdir -P #{service_path}'")
244
268
  sleep 3
245
- run_command("pkill -TERM -u #{kill_users.join(',')}") if kill_users.length > 0
269
+ run_command("pkill -TERM -u #{kill_users.join(",")}") if kill_users.length > 0
246
270
  run_command("pkill -TERM -f 'runsvdir -P #{service_path}'")
247
271
  sleep 3
248
- run_command("pkill -KILL -u #{kill_users.join(',')}") if kill_users.length > 0
272
+ run_command("pkill -KILL -u #{kill_users.join(",")}") if kill_users.length > 0
249
273
  run_command("pkill -KILL -f 'runsvdir -P #{service_path}'")
250
274
 
251
275
  get_all_services.each do |die_daemon_die|
252
276
  run_command("pkill -KILL -f 'runsv #{die_daemon_die}'")
253
277
  end
254
-
255
278
  log "Your config files have been backed up to #{backup_dir}."
256
279
  exit! 0
257
280
  end
@@ -260,16 +283,56 @@ module Omnibus
260
283
  cleanup_procs_and_nuke("/tmp/opt")
261
284
  end
262
285
 
263
- def cleanse(*args)
264
- log "This will delete *all* configuration, log, and variable data associated with this application.\n\n*** You have 60 seconds to hit CTRL-C ***\n\n"
265
- unless args[1] == "yes"
266
- sleep 60
286
+ def scary_cleanse_warning(*args)
287
+ just_do_it = args.include?("yes")
288
+ with_external = ARGV.include?("--with-external")
289
+ log <<EOM
290
+ *******************************************************************
291
+ * * * * * * * * * * * STOP AND READ * * * * * * * * * *
292
+ *******************************************************************
293
+ This command will delete *all* local configuration, log, and
294
+ variable data associated with #{display_name}.
295
+ EOM
296
+ if with_external
297
+ log <<EOM
298
+ This will also delete externally hosted #{display_name} data.
299
+ This means that any service you have configured as 'external'
300
+ will have any #{display_name} permanently deleted.
301
+ EOM
302
+ elsif not external_services.empty?
303
+ log <<EOM
304
+
305
+ Important note: If you also wish to delete externally hosted #{display_name}
306
+ data, please hit CTRL+C now and run '#{exe_name} cleanse --with-external'
307
+ EOM
308
+ end
309
+
310
+ unless just_do_it
311
+ data = with_external ? "local, and remote data" : "and local data"
312
+ log <<EOM
313
+
314
+ You have 60 seconds to hit CTRL-C before configuration,
315
+ logs, #{data} for this application are permanently
316
+ deleted.
317
+ *******************************************************************
318
+
319
+ EOM
320
+ begin
321
+ sleep 60
322
+ rescue Interrupt
323
+ log ""
324
+ exit 0
325
+ end
267
326
  end
268
- cleanup_procs_and_nuke("#{service_path}/* /tmp/opt #{data_path} #{etc_path} #{log_path}")
327
+ end
328
+
329
+ def cleanse(*args)
330
+ scary_cleanse_warning(*args)
331
+ cleanup_procs_and_nuke("#{service_path}/* /tmp/opt #{data_path} #{etc_path} #{log_path}", "cleanse")
269
332
  end
270
333
 
271
334
  def get_all_services_files
272
- Dir[File.join(sv_path, '*')]
335
+ Dir[File.join(sv_path, "*")]
273
336
  end
274
337
 
275
338
  def get_all_services
@@ -280,8 +343,10 @@ module Omnibus
280
343
  File.symlink?("#{service_path}/#{service_name}")
281
344
  end
282
345
 
283
- def run_sv_command(sv_cmd, service=nil)
346
+ def run_sv_command(sv_cmd, service = nil)
284
347
  exit_status = 0
348
+ sv_cmd = "1" if sv_cmd == "usr1"
349
+ sv_cmd = "2" if sv_cmd == "usr2"
285
350
  if service
286
351
  exit_status += run_sv_command_for_service(sv_cmd, service)
287
352
  else
@@ -296,10 +361,10 @@ module Omnibus
296
361
  def run_sv_command_for_service(sv_cmd, service_name)
297
362
  if service_enabled?(service_name)
298
363
  status = run_command("#{base_path}/init/#{service_name} #{sv_cmd}")
299
- return status.exitstatus
364
+ status.exitstatus
300
365
  else
301
366
  log "#{service_name} disabled" if sv_cmd == "status" && verbose
302
- return 0
367
+ 0
303
368
  end
304
369
  end
305
370
 
@@ -331,7 +396,7 @@ module Omnibus
331
396
  end
332
397
 
333
398
  # All other services respond normally to p-c-c * commands
334
- return true
399
+ true
335
400
  end
336
401
 
337
402
  # removed services are configured via the attributes file in
@@ -341,12 +406,7 @@ module Omnibus
341
406
  # not exist), we know that this will be a new server, and we don't
342
407
  # have to worry about pre-upgrade services hanging around. We can safely
343
408
  # return an empty array when running_config is nil
344
- if (cfg = running_config)
345
- key = package_name.gsub(/-/, '_')
346
- cfg[key]["removed_services"] || []
347
- else
348
- []
349
- end
409
+ running_package_config["removed_services"] || []
350
410
  end
351
411
 
352
412
  # hidden services are configured via the attributes file in
@@ -359,12 +419,7 @@ module Omnibus
359
419
  # not exist), we don't want to return nil, just return an empty array.
360
420
  # worse result with doing that is services that we don't want to show up in
361
421
  # c-s-c status will show up.
362
- if (cfg = running_config)
363
- key = package_name.gsub(/-/, '_')
364
- cfg[key]["hidden_services"] || []
365
- else
366
- []
367
- end
422
+ running_package_config["hidden_services"] || []
368
423
  end
369
424
 
370
425
  # translate the name from the config to the package name.
@@ -382,54 +437,146 @@ module Omnibus
382
437
 
383
438
  # returns nil when chef-server-running.json does not exist
384
439
  def running_config
385
- @running_config ||= begin
386
- if File.exists?("#{etc_path}/chef-server-running.json")
387
- JSON.parse(File.read("#{etc_path}/chef-server-running.json"))
388
- end
440
+ @running_config ||= if File.exist?("#{etc_path}/chef-server-running.json")
441
+ JSON.parse(File.read("#{etc_path}/chef-server-running.json"))
442
+ end
443
+ end
444
+
445
+ # Helper function that returns the hash of config hashes that have the key 'external' : true
446
+ # in the running config. If none exist it will return an empty hash.
447
+ def external_services
448
+ @external_services ||= running_package_config.select { |k, v| v.class == Hash and v["external"] == true }
449
+ end
450
+
451
+ # Helper function that returns true if an external service entry exists for
452
+ # the named service
453
+ def service_external?(service)
454
+ return false if service.nil?
455
+
456
+ external_services.key? service
457
+ end
458
+
459
+ # Gives package config from the running_config.
460
+ # If there is no running config or if package_name doens't
461
+ # reference a valid key, this will return an empty hash
462
+ def running_package_config
463
+ if (cfg = running_config)
464
+ cfg[package_name.gsub(/-/, "_")] || {}
465
+ else
466
+ {}
389
467
  end
390
468
  end
391
469
 
470
+ # This returns running_config[package][service].
471
+ #
472
+ # If there is no running_config or is no matching key
473
+ # it will return nil.
474
+ def running_service_config(service)
475
+ running_package_config[service]
476
+ end
477
+
392
478
  def remove_old_node_state
393
479
  node_cache_path = "#{base_path}/embedded/nodes/"
394
480
  status = run_command("rm -rf #{node_cache_path}")
395
- if ! status.success?
481
+ unless status.success?
396
482
  log "Could not remove cached node state!"
397
- exit! 1
483
+ exit 1
398
484
  end
399
485
  end
400
486
 
401
- def run_chef(attr_location, args='')
487
+ def run_chef(attr_location, args = "")
488
+ if @verbose
489
+ log_level = "-l debug"
490
+ elsif @quiet
491
+ # null formatter is awfully quiet, so let them know we're doing something.
492
+ log "Reconfiguring #{display_name}."
493
+ log_level = "-l fatal -F null"
494
+ else
495
+ log_level = ""
496
+ end
402
497
  remove_old_node_state
403
- cmd = "#{base_path}/embedded/bin/chef-client -z -c #{base_path}/embedded/cookbooks/solo.rb -j #{attr_location}"
498
+ cmd = "#{base_path}/embedded/bin/chef-client #{log_level} -z -c #{base_path}/embedded/cookbooks/solo.rb -j #{attr_location}"
404
499
  cmd += " #{args}" unless args.empty?
405
500
  run_command(cmd)
406
501
  end
407
502
 
408
503
  def show_config(*args)
409
- status = run_chef("#{base_path}/embedded/cookbooks/show-config.json", "-l fatal")
410
- if status.success?
411
- exit! 0
412
- else
413
- exit! 1
414
- end
504
+ status = run_chef("#{base_path}/embedded/cookbooks/show-config.json", "-l fatal -F null")
505
+ exit! status.success? ? 0 : 1
415
506
  end
416
507
 
417
- def reconfigure(exit_on_success=true)
508
+ def reconfigure(*args)
509
+ # args being passed to this command does not include the ones that are
510
+ # starting with "-". See #is_option? method. If it is starting with "-"
511
+ # then it is treated as a option and we need to look for them in ARGV.
512
+ check_license_acceptance(ARGV.include?("--accept-license"))
513
+
418
514
  status = run_chef("#{base_path}/embedded/cookbooks/dna.json")
419
515
  if status.success?
420
516
  log "#{display_name} Reconfigured!"
421
- exit! 0 if exit_on_success
517
+ exit! 0
422
518
  else
423
519
  exit! 1
424
520
  end
425
521
  end
426
522
 
523
+ def check_license_acceptance(override_accept = false)
524
+ license_guard_file_path = File.join(data_path, ".license.accepted")
525
+
526
+ # If the project does not have a license we do not have
527
+ # any license to accept.
528
+ return unless File.exist?(project_license_path)
529
+
530
+ unless File.exist?(license_guard_file_path)
531
+ if override_accept || ask_license_acceptance
532
+ FileUtils.mkdir_p(data_path)
533
+ FileUtils.touch(license_guard_file_path)
534
+ else
535
+ log "Please accept the software license agreement to continue."
536
+ exit(1)
537
+ end
538
+ end
539
+ end
540
+
541
+ def ask_license_acceptance
542
+ log "To use this software, you must agree to the terms of the software license agreement."
543
+
544
+ unless STDIN.tty?
545
+ log "Please view and accept the software license agreement, or pass --accept-license."
546
+ exit(1)
547
+ end
548
+
549
+ log "Press any key to continue."
550
+ user_input = STDIN.getch
551
+ user_input << STDIN.getch while STDIN.ready?
552
+ # No need to check for user input
553
+
554
+ system("less #{project_license_path}")
555
+
556
+ loop do
557
+ log "Type 'yes' to accept the software license agreement, or anything else to cancel."
558
+
559
+ user_input = STDIN.gets.chomp.downcase
560
+ case user_input
561
+ when "yes"
562
+ return true
563
+ else
564
+ log "You have not accepted the software license agreement."
565
+ return false
566
+ end
567
+ end
568
+ end
569
+
570
+ def project_license_path
571
+ File.join(base_path, "LICENSE")
572
+ end
573
+
427
574
  def tail(*args)
428
575
  # find /var/log -type f -not -path '*/sasl/*' | grep -E -v '(lock|@|tgz|gzip)' | xargs tail --follow=name --retry
429
- command = "find #{log_path}"
576
+ command = "find -L #{log_path}"
430
577
  command << "/#{args[1]}" if args[1]
431
- command << ' -type f'
432
- command << log_path_exclude.map { |path| " -not -path #{path}" }.join(' ')
578
+ command << " -type f"
579
+ command << log_path_exclude.map { |path| " -not -path '#{path}'" }.join(" ")
433
580
  command << " | grep -E -v '#{log_exclude}' | xargs tail --follow=name --retry"
434
581
 
435
582
  system(command)
@@ -444,16 +591,17 @@ module Omnibus
444
591
  exit_status = 0
445
592
  get_all_services.each do |service_name|
446
593
  next if !service.nil? && service_name != service
594
+
447
595
  if service_enabled?(service_name)
448
- pidfile="#{sv_path}/#{service_name}/supervise/pid"
449
- pid=File.read(pidfile).chomp if File.exists?(pidfile)
596
+ pidfile = "#{sv_path}/#{service_name}/supervise/pid"
597
+ pid = File.read(pidfile).chomp if File.exist?(pidfile)
450
598
  if pid.nil? || !is_integer?(pid)
451
599
  log "could not find #{service_name} runit pidfile (service already stopped?), cannot attempt SIGKILL..."
452
600
  status = run_command("#{base_path}/init/#{service_name} stop")
453
601
  exit_status = status.exitstatus if exit_status == 0 && !status.success?
454
602
  next
455
603
  end
456
- pgrp=get_pgrp_from_pid(pid)
604
+ pgrp = get_pgrp_from_pid(pid)
457
605
  if pgrp.nil? || !is_integer?(pgrp)
458
606
  log "could not find pgrp of pid #{pid} (not running?), cannot attempt SIGKILL..."
459
607
  status = run_command("#{base_path}/init/#{service_name} stop")
@@ -461,8 +609,8 @@ module Omnibus
461
609
  next
462
610
  end
463
611
  run_command("#{base_path}/init/#{service_name} stop")
464
- pids=get_pids_from_pgrp(pgrp)
465
- if !pids.empty?
612
+ pids = get_pids_from_pgrp(pgrp)
613
+ unless pids.empty?
466
614
  log "found stuck pids still running in process group: #{pids}, sending SIGKILL" unless pids.empty?
467
615
  sigkill_pgrp(pgrp)
468
616
  end
@@ -475,14 +623,14 @@ module Omnibus
475
623
  end
476
624
 
477
625
  def help(*args)
478
- log "#{$0}: command (subcommand)\n"
626
+ log "#{exe_name}: command (subcommand)\n"
479
627
  command_map.keys.sort.each do |command|
480
628
  log command
481
629
  log " #{command_map[command][:desc]}"
482
630
  end
483
631
  category_command_map.each do |category, commands|
484
632
  # Remove "-" and replace with spaces in category and capalize for output
485
- category_string = category.gsub("-", " ").split.map(&:capitalize).join(' ')
633
+ category_string = category.gsub("-", " ").split.map(&:capitalize).join(" ")
486
634
  log "#{category_string} Commands:\n"
487
635
 
488
636
  # Print each command in this category
@@ -491,49 +639,65 @@ module Omnibus
491
639
  log " #{commands[command][:desc]}"
492
640
  end
493
641
  end
494
- exit! 1
642
+ # Help is not an error so exit with 0. In cases where we display help as a result of an error
643
+ # the framework will handle setting proper exit code.
644
+ exit! 0
495
645
  end
496
646
 
497
- # Set options. Silently ignore bad options.
498
- # This allows the test subcommand to pass on pedant options
499
- def parse_options!(args)
500
- args.each do |option|
647
+ # Set global options and remove them from the args list we pass
648
+ # into commands.
649
+ def parse_options(args)
650
+ args.select do |option|
501
651
  case option
652
+ when "--quiet", "-q"
653
+ @quiet = true
654
+ false
502
655
  when "--verbose", "-v"
503
656
  @verbose = true
657
+ false
504
658
  end
505
659
  end
506
660
  end
507
661
 
508
662
  # If it begins with a '-', it is an option.
509
663
  def is_option?(arg)
510
- arg && arg[0] == '-'
664
+ arg && arg[0] == "-"
511
665
  end
512
666
 
513
667
  # retrieves the commmand from either the command_map
514
668
  # or the category_command_map, if the command is not found
515
669
  # return nil
516
670
  def retrieve_command(command_to_run)
517
- if command_map.has_key?(command_to_run)
671
+ if command_map.key?(command_to_run)
518
672
  command_map[command_to_run]
519
673
  else
520
674
  command = nil
521
675
  category_command_map.each do |category, commands|
522
- command = commands[command_to_run] if commands.has_key?(command_to_run)
676
+ command = commands[command_to_run] if commands.key?(command_to_run)
523
677
  end
524
678
  # return the command, or nil if it wasn't found
525
679
  command
526
680
  end
527
681
  end
528
682
 
683
+ # Previously this would exit immediately with the provided
684
+ # exit code; however this would prevent post-run hooks from continuing
685
+ # Instead, we'll just track whether a an exit was requested and use that
686
+ # to determine how we exit from 'run'
529
687
  def run(args)
530
688
  # Ensure Omnibus related binaries are in the PATH
531
689
  ENV["PATH"] = [File.join(base_path, "bin"),
532
- File.join(base_path, "embedded","bin"),
533
- ENV['PATH']].join(":")
690
+ File.join(base_path, "embedded", "bin"),
691
+ ENV["PATH"]].join(":")
534
692
 
535
693
  command_to_run = args[0]
536
694
 
695
+ ## when --help is run as the command itself, we need to strip off the
696
+ ## `--` to ensure the command maps correctly.
697
+ if command_to_run == "--help"
698
+ command_to_run = "help"
699
+ end
700
+
537
701
  # This piece of code checks if the argument is an option. If it is,
538
702
  # then it sets service to nil and adds the argument into the options
539
703
  # argument. This is ugly. A better solution is having a proper parser.
@@ -549,31 +713,208 @@ module Omnibus
549
713
  service = args[1]
550
714
  end
551
715
 
552
- # returns either hash content of comamnd or nil
716
+ # returns either hash content of command or nil
553
717
  command = retrieve_command(command_to_run)
554
-
555
718
  if command.nil?
556
719
  log "I don't know that command."
557
720
  if args.length == 2
558
- log "Did you mean: #{$0} #{service} #{command_to_run}?"
721
+ log "Did you mean: #{exe_name} #{service} #{command_to_run}?"
559
722
  end
560
723
  help
724
+ Kernel.exit 1
561
725
  end
562
726
 
563
727
  if args.length > 1 && command[:arity] != 2
564
728
  log "The command #{command_to_run} does not accept any arguments"
565
- exit! 2
729
+ Kernel.exit 2
566
730
  end
567
731
 
568
- parse_options! options
732
+ parse_options options
733
+ @force_exit = false
734
+ exit_code = 0
735
+
736
+ run_global_pre_hooks
569
737
 
570
- method_to_call = command_to_run.gsub(/-/, '_')
571
738
  # Filter args to just command and service. If you are loading
572
739
  # custom commands and need access to the command line argument,
573
740
  # use ARGV directly.
574
741
  actual_args = [command_to_run, service].reject(&:nil?)
575
- self.send(method_to_call.to_sym, *actual_args)
742
+ if command_pre_hook(*actual_args)
743
+ method_to_call = to_method_name(command_to_run)
744
+ begin
745
+ ret = send(method_to_call, *actual_args)
746
+ rescue SystemExit => e
747
+ @force_exit = true
748
+ ret = e.status
749
+ end
750
+ command_post_hook(*actual_args)
751
+ exit_code = ret unless ret.nil?
752
+ else
753
+ exit_code = 8
754
+ @force_exit = true
755
+ end
756
+
757
+ if @force_exit
758
+ Kernel.exit exit_code
759
+ else
760
+ exit_code
761
+ end
762
+ end
763
+
764
+ def run_global_pre_hooks
765
+ @global_pre_hooks.each do |hook_name, method_name|
766
+
767
+ send(method_name)
768
+ rescue => e
769
+ $stderr.puts("Global pre-hook '#{hook_name}' failed with: '#{e.message}'")
770
+ exit(1)
771
+
772
+ end
576
773
  end
577
774
 
775
+ # Below are some basic command hooks that do the right thing
776
+ # when a service is configured as external via [package][service
777
+
778
+ # If a command has a pre-hook defined we will run it.
779
+ # Otherwise, if it is a run-sv command and the service it refers to
780
+ # is an external service, we will show an error since we
781
+ # can't control external services from here.
782
+ #
783
+ # If any pre-hook returns false, it will prevent execution of the command
784
+ # and exit the command with exit code 8.
785
+ def command_pre_hook(*args)
786
+ command = args.shift
787
+ method = to_method_name("#{command}_pre_hook")
788
+ if respond_to?(method)
789
+ send(method, *args)
790
+ else
791
+ return true if args.empty?
792
+
793
+ if SV_COMMAND_NAMES.include? command
794
+ if service_external? args[0]
795
+ log error_external_service(command, args[0])
796
+ return false
797
+ end
798
+ end
799
+ true
800
+ end
801
+ end
802
+
803
+ # Executes after successful completion of a command
804
+ # If a post-hook provides a numeric return code, it will
805
+ # replace the return/exit of the original command
806
+ def command_post_hook(*args)
807
+ command = args.shift
808
+ method = to_method_name("#{command}_post_hook")
809
+ if respond_to?(method)
810
+ send(method, *args)
811
+ end
812
+ end
813
+
814
+ # If we're listing status for all services and have external
815
+ # services to show, we'll include an output header to show that
816
+ # we're reporting internal services
817
+ def status_pre_hook(service = nil)
818
+ log_internal_service_header if service.nil?
819
+ true
820
+ end
821
+
822
+ # Status gets its own hook because each externalized service will
823
+ # have its own things to do in order to report status.
824
+ # As above, we may also include an output header to show that we're
825
+ # reporting on external services.
826
+ #
827
+ # Your callback for this function should be in the form
828
+ # 'external_status_#{service_name}(detail_level)
829
+ # where detail_level is :sparse|:verbose
830
+ # :sparse is used when it's a summary service status list, eg
831
+ # "$appname-ctl status"
832
+ # :verbose is used when the specific service has been named, eg
833
+ # "$appname-ctl status postgresql"
834
+ def status_post_hook(service = nil)
835
+ if service.nil?
836
+ log_external_service_header
837
+ external_services.each_key do |service_name|
838
+ status = send(to_method_name("external_status_#{service_name}"), :sparse)
839
+ log status
840
+ end
841
+ else
842
+ # Request verbose status if the service is asked for by name.
843
+ if service_external?(service)
844
+ status = send(to_method_name("external_status_#{service}"), :verbose)
845
+ log status
846
+ end
847
+ end
848
+ end
849
+
850
+ # Data cleanup requirements for external services aren't met by the standard
851
+ # 'nuke /var/opt' behavior - this hook allows each service to perform its own
852
+ # 'cleanse' operations.
853
+ #
854
+ # Your callback for this function should be in the
855
+ # form 'external_cleanup_#{service_name}(do_clean)
856
+ # where do_cliean is true if the delete should actually be
857
+ # performed, and false if it's expected to inform the user how to
858
+ # perform the data cleanup without doing any cleanup itself.
859
+ def cleanse_post_hook(*args)
860
+ external_services.each_key do |service_name|
861
+ perform_delete = ARGV.include?("--with-external")
862
+ if perform_delete
863
+ log "Deleting data from external service: #{service_name}"
864
+ end
865
+ send(to_method_name("external_cleanse_#{service_name}"), perform_delete)
866
+ end
867
+ end
868
+
869
+ # Add some output headers if we have external services enabled
870
+ def service_list_pre_hook
871
+ log_internal_service_header
872
+ true
873
+ end
874
+
875
+ # Capture external services in the output list as well.
876
+ def service_list_post_hook
877
+ log_external_service_header
878
+ external_services.each do |name, settings|
879
+ log " > #{name} on #{settings["vip"]}"
880
+ end
881
+ end
882
+
883
+ def error_external_service(command, service)
884
+ <<EOM
885
+ -------------------------------------------------------------------
886
+ The service #{service} is running externally and cannot be managed
887
+ vi chef-server-ctl. Please log into #{external_services[service]["vip"]}
888
+ to manage it directly.
889
+ -------------------------------------------------------------------
890
+ EOM
891
+ end
892
+
893
+ def format_multiline_message(indent, message)
894
+ if message.class == String
895
+ message = message.split("\n")
896
+ end
897
+ spaces = " " * indent
898
+ message.map! { |line| "#{spaces}#{line.strip}" }
899
+ message.join("\n")
900
+ end
901
+
902
+ def log_internal_service_header
903
+ # Don't decorate output unless we have
904
+ # external services to report on.
905
+ return if external_services.empty?
906
+
907
+ log "-------------------"
908
+ log " Internal Services "
909
+ log "-------------------"
910
+ end
911
+
912
+ def log_external_service_header
913
+ return if external_services.empty?
914
+
915
+ log "-------------------"
916
+ log " External Services "
917
+ log "-------------------"
918
+ end
578
919
  end
579
920
  end