omnibus-ctl 0.3.5 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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