omnibus-ctl 0.3.6 → 0.6.9

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