From 9dc635853ca6bf8fe4a948aacd63161bbad2e667 Mon Sep 17 00:00:00 2001 From: Mark Anderson Date: Thu, 9 Aug 2018 00:14:15 -0700 Subject: [PATCH] WIP Signed-off-by: Mark Anderson --- .gitignore | 1 + lib/omnibus-ctl/command/base.rb | 90 ++++++++++ lib/omnibus-ctl/command/runit.rb | 55 ++++++ lib/omnibus-ctl/config.rb | 32 ++++ lib/omnibus-ctl/ctl.rb | 233 +++++++++++++++++++++++++ lib/omnibus-ctl/log.rb | 17 ++ lib/omnibus-ctl/mixin/config.rb | 26 +++ lib/omnibus-ctl/mixin/erl_env.rb | 22 +++ lib/omnibus-ctl/mixin/interactive.rb | 99 +++++++++++ lib/omnibus-ctl/mixin/paths.rb | 17 ++ lib/omnibus-ctl/mixin/recommender.rb | 25 +++ lib/omnibus-ctl/mixin/retry.rb | 31 ++++ lib/omnibus-ctl/mixin/run_chef.rb | 48 +++++ lib/omnibus-ctl/mixin/run_command.rb | 25 +++ lib/omnibus-ctl/mixin/runit.rb | 42 +++++ lib/omnibus-ctl/mixin/service_utils.rb | 82 +++++++++ lib/omnibus-ctl/mixin/tcp_utils.rb | 24 +++ lib/omnibus-ctl/version.rb | 2 +- omnibus-ctl.gemspec | 13 +- spec/command/base_spec.rb | 4 + spec/spec_helper.rb | 38 +++- 21 files changed, 922 insertions(+), 4 deletions(-) create mode 100644 lib/omnibus-ctl/command/base.rb create mode 100644 lib/omnibus-ctl/command/runit.rb create mode 100644 lib/omnibus-ctl/config.rb create mode 100644 lib/omnibus-ctl/ctl.rb create mode 100644 lib/omnibus-ctl/log.rb create mode 100644 lib/omnibus-ctl/mixin/config.rb create mode 100644 lib/omnibus-ctl/mixin/erl_env.rb create mode 100644 lib/omnibus-ctl/mixin/interactive.rb create mode 100644 lib/omnibus-ctl/mixin/paths.rb create mode 100644 lib/omnibus-ctl/mixin/recommender.rb create mode 100644 lib/omnibus-ctl/mixin/retry.rb create mode 100644 lib/omnibus-ctl/mixin/run_chef.rb create mode 100644 lib/omnibus-ctl/mixin/run_command.rb create mode 100644 lib/omnibus-ctl/mixin/runit.rb create mode 100644 lib/omnibus-ctl/mixin/service_utils.rb create mode 100644 lib/omnibus-ctl/mixin/tcp_utils.rb create mode 100644 spec/command/base_spec.rb diff --git a/.gitignore b/.gitignore index 4040c6c..f1a2dae 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .bundle Gemfile.lock pkg/* +vendor diff --git a/lib/omnibus-ctl/command/base.rb b/lib/omnibus-ctl/command/base.rb new file mode 100644 index 0000000..28c6463 --- /dev/null +++ b/lib/omnibus-ctl/command/base.rb @@ -0,0 +1,90 @@ +# +# Base infrastructure for a subcommand +# + +# +# An expensive require can slow the whole system down; minimize what goes here at all costs +# +require 'mixlib/cli' +require 'omnibus/mixin/recommender' +require 'omnibus/mixin/interactive' +require 'omnibus/mixin/run_command' +require 'omnibus/mixin/runit' +require 'omnibus/mixin/paths' +require 'omnibus/config' + +module Omnibus + module Command + class Base + # + # Register declares + # + class << self; attr_accessor :available_commands end + available_commands = [] + def self.register_command(class_name, command_name, command_groups = []) + available_commands += {name: command_name, groups: command_groups} + end + + # + # Load is called by the ctl before #run is called + # + def load(basic_config = {}) + + # We put the requires here rather than in deps since we want + # subclasses to define deps without needing to call super. + require 'fileutils' + require 'tempfile' + require 'highline' + require 'omnibus' + require 'omnibus/log' + log_level = if basic_config[:verbose] + :debug + else + :info + end + Omnibus::Log.level = log_level + HighLine.use_color = $stdout.tty? + Omnibus.load + deps + end + + # + # Called before #run + # Subclasses will override this with their own requires. + # + def deps + + end + + def help + puts opt_parser.to_s + if extended_help.length > 0 + puts "\n#{extended_help}" + end + end + alias :show_help :help + + # + # Returns a String that is appended to the end of the help + # output for the command, allowing commands to provide more help + # text than a simple list of options. + # + def extended_help + "" + end + + def log(msg) + $stdout.puts msg unless config[:quiet] + end + + def warn(msg) + $stderr.puts HighLine.color(msg, :yellow) + end + + def err(msg) + $stderr.puts HighLine.color(msg, :red) + end + + end + end +end # module Omnibus diff --git a/lib/omnibus-ctl/command/runit.rb b/lib/omnibus-ctl/command/runit.rb new file mode 100644 index 0000000..500ced5 --- /dev/null +++ b/lib/omnibus-ctl/command/runit.rb @@ -0,0 +1,55 @@ +require 'omnibus/command/base' + +module Omnibus + module Command + # + # RunitBase is the base class for all runit-based commands. Since + # all of the runit based commands simply run a different sv + # command, we define them all in this file. + # + class RunitBase < Base + include Omnibus::Mixin::ServiceUtils + + def self.include_runitopts + include_stdopts + end + + def sv_command + raise NotImplemented + end + + def run(args) + parse_options(args) + services = cli_arguments + if services_exist?(services) + run_sv_command(sv_command, services) + else + recommend_services(services) + 1 + end + end + end + + # + # Runit supports more commands that these, but these + # are the commands supported by our -ctl style commands in + # other products + # + RUNIT_COMMANDS = %w{start stop restart once hup + term int kill usr1 usr2} + + RUNIT_COMMANDS.each do |cmd_name| + class_name = cmd_name.delete("-").capitalize + class_eval(<<-CLASS) + class #{class_name} < RunitBase + register_command cmd_name, [:runit] + banner "Usage: #{Omnibus.config.cmd_name} #{cmd_name} [SERVICE..] (options)" + include_runitopts + def sv_command + "#{cmd_name}" + end + end +CLASS + end + end +end diff --git a/lib/omnibus-ctl/config.rb b/lib/omnibus-ctl/config.rb new file mode 100644 index 0000000..77e802b --- /dev/null +++ b/lib/omnibus-ctl/config.rb @@ -0,0 +1,32 @@ +# +# +# + + +# This taken from https://6ftdan.com/allyourdev/2015/03/07/configuration-with-a-singleton-instance/ +# Revisit whether openstruct is the right choice, or mixlib config +require 'ostruct' +class Omnibus::OmnibusConfig < OpenStruct + method_missing(:cmd_name) || "omnibus-ctl" + method_missing(:base_path) || "/opt/omnibus" +end + +module Omnibus + module Config + class << self + def config + @config ||= OmnibusConfig.new + end + end + end +end + +Omnibus.Config.config + +# ALL_SERVICES = %w{leaderl epmd etcd postgresql elasticsearch} +# ALL_SYSPARAMS = %w{disks} +# BASE_PATH = "/opt/opscode" +# CONFIG_PATH = "/etc/opscode/chef-server.rb" +# SERVICE_PATH = "#{BASE_PATH}/service" +# SV_PATH = "#{BASE_PATH}/sv" +# SV_INIT_PATH = "#{BASE_PATH}/init" diff --git a/lib/omnibus-ctl/ctl.rb b/lib/omnibus-ctl/ctl.rb new file mode 100644 index 0000000..2eb1a19 --- /dev/null +++ b/lib/omnibus-ctl/ctl.rb @@ -0,0 +1,233 @@ +require 'mixlib/cli' +require 'libcb/mixin/recommender' + +module Omnibus + # + # The Ctl class is the main entry point for chef-backend-ctl. This + # class is responsible for handling dispatching into the subcommands + # and handling basic options such as help and version. + # + # Command dispatching happens via a Hash of command names to a Hash + # containing the class name that contains the subcommand, + # description, and path to require to instantiate the subcommand + # class. + # + class Ctl + include Mixlib::CLI + + banner <<-USAGE +Usage: + #{Config.cmd_name} -h/--help + #{Config.cmd_name} -v/--version + #{Config.cmd_name} COMMAND [arguments...] [options...] +USAGE + + # + # We use a static list of subcommand names to the file to require + # and the class to instantiate. + # + + COMMAND_MAP = + + include Omnibus::Mixin::Recommender + + attr_reader :argv + def initialize(argv) + @argv = argv + super() + end + + # + # Having multiple classes controlling mixlib-cli gets a bit hairy. + # For now, we use manual option handling in this top-level + # dispatcher since we only need a few options at this leven. + # + def quiet? + argv.include?("--quiet") || argv.include?("-q") + end + + def verbose? + argv.include?("--verbose") || argv.include?("-V") + end + + def version_requested? + argv.include?("--version") || argv.include?("-v") + end + + def help_requested? + argv.include?("--help") || argv.include?("-h") + end + + def option?(arg) + arg[0] == "-" + end + + def run + subcommand, *args = argv + if subcommand.nil? || option?(subcommand) + if version_requested?() + show_version_info + else + show_help + end + exit 0 + else + exit_status = run_subcommand(subcommand, args) + if exit_status.is_a? Integer + exit exit_status + else + exit 0 + end + end + end + + def run_help(args) + pos_args = args.reject { |a| option?(a) } + if pos_args.empty? + show_help + else + subcommand_name = pos_args.first + if subcommand_exists?(subcommand_name) + subcommand = load_subcommand(subcommand_name) + if subcommand.respond_to?(:help) + subcommand.help + else + show_help + end + else + handle_unknown_subcommand(subcommand_name) + end + end + end + + def handle_unknown_subcommand(command_name, args = []) + $stderr.puts "Unknown command: #{command_name}" + if rec = find_recommendation(command_name, COMMAND_MAP.keys) + $stderr.puts "\nDid you mean:" + $stderr.puts " #{rec}" + elsif args.first && subcommand_exists?(args.first) + $stderr.puts "\nDid you mean:" + $stderr.puts " chef-backend-ctl #{args.first} #{command_name}" + else + show_help + end + end + + def load_subcommand(subcommand_name) + s = COMMAND_MAP[subcommand_name] + require s[:path] + subcommand_class = Omnibus::Command.const_get(s[:class_name]) + subcommand_class.new + end + + def run_subcommand(subcommand_name, args) + # We handle "help" specially so that we can keep all of the + # command dispatching in once place. + if subcommand_name == "help" + run_help(args) + return 0 + end + + if subcommand_exists?(subcommand_name) + subcommand = load_subcommand(subcommand_name) + + if help_requested? + subcommand.help + 0 + elsif version_requested? + show_version_info + 0 + else + subcommand.load(verbose: verbose?, quiet: quiet?) + run_with_pretty_errors(subcommand, args) + end + else + handle_unknown_subcommand(subcommand_name, args) + exit(1) + end + # Errors that occur in this function are likely the result of a major + # programming error (missing require statements, bad requires) + rescue StandardError + $stderr.puts < e + $stderr.puts "An unexpected error occurred:" + $stderr.puts " #{e}\n" + if verbose? + e.backtrace.each do |line| + $stderr.puts "#{line}" + end + else + $stderr.puts "For more information run the command with the --verbose flag." + end + 1 + end + + WRAP_AT = 80 + def show_help + print banner + max_name_length = COMMAND_MAP.map { |k, v| k.length }.max + puts "\nInstall and Configuration Commands:\n\n" + print_map(INSTALL_COMMANDS, max_name_length) + puts "\nCluster-level Commands:\n\n" + print_map(CLUSTER_COMMANDS, max_name_length) + puts "\nService-level Commands:\n\n" + print_map(SERVICE_COMMANDS, max_name_length) + if verbose? + puts "\nInternal Commands (USE WITH CAUTION)\n" + print_map(INTERNAL_COMMANDS, max_name_length) + end + end + + def print_map(command_map, max_name_length) + command_map.each do |name, spec| + name_for_print = name.ljust(max_name_length + 2) + puts " #{name_for_print}#{wrap_col(spec[:description], max_name_length + 4, WRAP_AT)}" + end + end + + def wrap_col(str, start_pos, end_pos) + col_size = (end_pos - start_pos) + return str if str.length <= col_size + out_buf = "" + cur_line = "" + words = str.split(/\s+/) + words.each do |w| + if cur_line.empty? + cur_line << "#{w}" + elsif (cur_line.length + w.length + 1) < col_size + cur_line << " #{w}" + else + if !out_buf.empty? + out_buf << " " * (start_pos) + end + out_buf << cur_line.dup + out_buf << "\n" + cur_line = w + end + end + out_buf << " " * (start_pos) + out_buf << cur_line.dup + out_buf << "\n" + out_buf + end + + def subcommand_exists?(name) + COMMAND_MAP.has_key?(name) + end + + VERSION_FILE = "/opt/chef-backend/version-manifest.txt" + def show_version_info + print File.open(VERSION_FILE) { |f| f.readline } + end + end +end diff --git a/lib/omnibus-ctl/log.rb b/lib/omnibus-ctl/log.rb new file mode 100644 index 0000000..d57bbe7 --- /dev/null +++ b/lib/omnibus-ctl/log.rb @@ -0,0 +1,17 @@ +# Copyright (c) 2016 Chef Software, Inc. +# +# All Rights Reserved +# + +require 'mixlib/log' + +module Omnibus + if defined?(::Chef::Log) + Log = ::Chef::Log + else + class Log + extend Mixlib::Log + Mixlib::Log::Formatter.show_time = false + end + end +end diff --git a/lib/omnibus-ctl/mixin/config.rb b/lib/omnibus-ctl/mixin/config.rb new file mode 100644 index 0000000..be92959 --- /dev/null +++ b/lib/omnibus-ctl/mixin/config.rb @@ -0,0 +1,26 @@ +# +# +# + +# Revisit whether openstruct is the right choice, or mixlib config +require 'ostruct' +class Omnibus::Mixin::OmnibusConfig < OpenStruct + +module Omnibus + module Mixin + module Config + class << self + def config + @config ||= + + + ALL_SERVICES = %w{leaderl epmd etcd postgresql elasticsearch} + ALL_SYSPARAMS = %w{disks} + BASE_PATH = "/opt/opscode" + CONFIG_PATH = "/etc/opscode/chef-server.rb" + SERVICE_PATH = "#{BASE_PATH}/service" + SV_PATH = "#{BASE_PATH}/sv" + SV_INIT_PATH = "#{BASE_PATH}/init" + end + end +end diff --git a/lib/omnibus-ctl/mixin/erl_env.rb b/lib/omnibus-ctl/mixin/erl_env.rb new file mode 100644 index 0000000..bc452a7 --- /dev/null +++ b/lib/omnibus-ctl/mixin/erl_env.rb @@ -0,0 +1,22 @@ +# TODO +# Like the path module this is config information and should be configurable +# + +module Omnibus + module Mixin + module ErlEnv + def set_erl_env + ENV['ERL_EPMD_PORT'] = epmd_port.to_s + ENV['ERL_EPMD_ADDRESS'] = '127.0.0.1' + end + + def epmd_bin + File.join(LibCB.node['chef-backend']['install_path'], 'embedded/bin/epmd') + end + + def epmd_port + LibCB.node['chef-backend']['epmd']['port'] + end + end + end +end diff --git a/lib/omnibus-ctl/mixin/interactive.rb b/lib/omnibus-ctl/mixin/interactive.rb new file mode 100644 index 0000000..1414bc0 --- /dev/null +++ b/lib/omnibus-ctl/mixin/interactive.rb @@ -0,0 +1,99 @@ + +module Omnibus + module Mixin + module Interactive + def confirm(explanation = nil, allow_override = true) + require 'highline/import' + if allow_override + if config[:yes] + return true + else + if !$stdin.tty? + puts "To run this command non-interactively, you must pass in the parameter --yes to force it to continue." + return false + end + end + else + if !$stdin.tty? + puts "This command requires explicit approval to continue and must be run interactively." + return false + end + end + + unless explanation.nil? + puts "" + puts explanation + puts "" + end + + confirmed = ask("Are you sure you wish to proceed? Type 'proceed' to continue, anything else to cancel.") + confirmed.downcase == 'proceed' + end + + def confirm!(explanation = nil, allow_override = true) + if !confirm(explanation, allow_override) + puts "Canceling operation" + exit(4) + end + true + end + + # + # The following functions where taken from omnibus-ctl to ensure + # that we present a similar experience with respect to license + # acceptance as other Chef Software tools. + # + def check_license_acceptance(override_accept = false) + license_guard_file_path = "/var/opt/chef-backend/.license.accepted" + + # If the project does not have a license we do not have + # any license to accept. + return true unless File.exist?(project_license_path) + + if !File.exist?(license_guard_file_path) + if override_accept || ask_license_acceptance + FileUtils.mkdir_p("/var/opt/chef-backend") + FileUtils.touch(license_guard_file_path) + else + log "Please accept the software license agreement to continue." + exit(1) + end + end + true + end + + private + + def ask_license_acceptance + require 'io/console' + require 'io/wait' + require 'highline/import' + + log "To use this software, you must agree to the terms of the software license agreement." + + if !$stdin.tty? + log "Please view and accept the software license agreement, or pass --accept-license." + exit(1) + end + + log "Press any key to continue." + user_input = $stdin.getch + user_input << $stdin.getch while $stdin.ready? + + pager = ENV["PAGER"] || "less" + system("#{pager} #{project_license_path}") + + if ask("Type 'yes' to accept the software license agreement, or anything else to cancel: ") == "yes" + true + else + log "You have not accepted the software license agreement." + false + end + end + + def project_license_path + "/opt/chef-backend/LICENSE" + end + end + end +end diff --git a/lib/omnibus-ctl/mixin/paths.rb b/lib/omnibus-ctl/mixin/paths.rb new file mode 100644 index 0000000..cbec1ba --- /dev/null +++ b/lib/omnibus-ctl/mixin/paths.rb @@ -0,0 +1,17 @@ +# +# TODO This is config information, and probably should be outsourced +# + +module Omnibus + module Mixin + module Paths + ALL_SERVICES = %w{leaderl epmd etcd postgresql elasticsearch} + ALL_SYSPARAMS = %w{disks} + BASE_PATH = "/opt/opscode" + CONFIG_PATH = "/etc/opscode/chef-server.rb" + SERVICE_PATH = "#{BASE_PATH}/service" + SV_PATH = "#{BASE_PATH}/sv" + SV_INIT_PATH = "#{BASE_PATH}/init" + end + end +end diff --git a/lib/omnibus-ctl/mixin/recommender.rb b/lib/omnibus-ctl/mixin/recommender.rb new file mode 100644 index 0000000..f923709 --- /dev/null +++ b/lib/omnibus-ctl/mixin/recommender.rb @@ -0,0 +1,25 @@ + +module Omnibus + module Mixin + module Recommender + MAX_EDIT_DISTANCE = 4 + + def find_recommendation(arg, candidates) + require 'levenshtein' + possible = {} + candidates.each do |s| + possible[s] = Levenshtein.distance(arg, s) + end + best = nil + min = nil + possible.each do |k, v| + if v < MAX_EDIT_DISTANCE && (!min || v < min) + best = k + min = v + end + end + best + end + end + end +end diff --git a/lib/omnibus-ctl/mixin/retry.rb b/lib/omnibus-ctl/mixin/retry.rb new file mode 100644 index 0000000..a73b178 --- /dev/null +++ b/lib/omnibus-ctl/mixin/retry.rb @@ -0,0 +1,31 @@ +module Omnibus + module Mixin + # + # Helper functions for dealing with operations + # that may fail and should be retried + # + module Retry + def try_with_retries(retry_count, opts = {}, &block) + rescue_class = opts[:exception_class] || StandardError + retries = 0 + begin + yield + rescue rescue_class => e + if retries < retry_count + retries += 1 + if opts[:sleep_time] + sleep opts[:sleep_time] + end + retry + else + case opts[:on_fail] + when :ignore + else + raise e + end + end + end + end + end + end +end diff --git a/lib/omnibus-ctl/mixin/run_chef.rb b/lib/omnibus-ctl/mixin/run_chef.rb new file mode 100644 index 0000000..9745d79 --- /dev/null +++ b/lib/omnibus-ctl/mixin/run_chef.rb @@ -0,0 +1,48 @@ +module Omnibus + module Mixin + module RunChef + def run_chef(opts = {}) + remove_old_node_state + log_level = if opts.has_key?(:log_level) + "-l #{opts[:log_level]}" + elsif config[:verbose] + "-l debug" + elsif config[:quiet] + "-l fatal" + else + "" + end + + formatter = if opts.has_key?(:formatter) + "-F #{opts[:formatter]}" + elsif config[:quiet] + "-F null" + else + "" + end + + override = opts.has_key?(:override_run_list) ? "-o #{opts[:override_run_list]}" : "" + + int_json_file = nil + if opts.has_key?(:override_attributes) + int_json_file = Tempfile.new('cb-attributes') + int_json_file.write(opts[:override_attributes].to_json) + int_json_file.flush + opts[:json_file] = int_json_file.path + end + + json = opts.has_key?(:json_file) ? "-j #{opts[:json_file]}" : "" + + cmd = "#{BASE_PATH}/embedded/bin/chef-client #{log_level} #{formatter} -z -c #{BASE_PATH}/embedded/cookbooks/solo.rb #{json} #{override}" + cmd += " #{opts[:args]}" unless opts[:args].nil? || opts[:args].empty? + run_command(cmd) + ensure + if int_json_file + int_json_file.close + int_json_file.unlink + end + end + end + end +end + diff --git a/lib/omnibus-ctl/mixin/run_command.rb b/lib/omnibus-ctl/mixin/run_command.rb new file mode 100644 index 0000000..ebe6770 --- /dev/null +++ b/lib/omnibus-ctl/mixin/run_command.rb @@ -0,0 +1,25 @@ +require 'libcb/log' + +module Omnibus + module Mixin + module RunCommand + def run_command(cmd, opts = {}) + cmd_opts = {} + if opts[:quiet] + cmd_opts[:out] = "/dev/null" + cmd_opts[:err] = "/dev/null" + end + Omnibus::Log.debug("Running cmd: #{filter_command(cmd)} (options: #{cmd_opts})") + system(cmd, cmd_opts) + $? + end + + # Simple filtering for pgsql commands that might have passwords. + # The passwords that we generate don't have spaces; however, + # this filtering may fail on user supplied passwords. + def filter_command(cmd) + cmd.gsub(/(password=)(\w*)\b/, '\1') + end + end + end +end diff --git a/lib/omnibus-ctl/mixin/runit.rb b/lib/omnibus-ctl/mixin/runit.rb new file mode 100644 index 0000000..d91af14 --- /dev/null +++ b/lib/omnibus-ctl/mixin/runit.rb @@ -0,0 +1,42 @@ +require 'libcb/mixin/run_command' +require 'libcb/mixin/paths' + +module Omnibus + module Mixin + module Runit + + include Omnibus::Mixin::RunCommand + include Omnibus::Mixin::Paths + + def run_sv_command(cmd, service_list = [], opts = {}) + service_list = Array(service_list) + exit_status = 0 + cmd = "1" if cmd == "usr1" + cmd = "2" if cmd == "usr2" + service_list = ALL_SERVICES if service_list.empty? + service_list.each do |service_name| + exit_status += run_sv_command_for_service(cmd, service_name, opts).exitstatus + end + exit_status + end + + def remove_down_file(service) + FileUtils.rm_rf(File.join([Paths::SERVICE_PATH,service,"down"])) + end + + def run_sv_command_for_service(cmd, service_name, opts = {}) + run_command("#{init_path(service_name)} #{cmd}", opts) + end + + def init_path(svc) + File.join([Omnibus::Mixin::Paths::SV_INIT_PATH,svc]" + end + + def known_services + Dir.glob(init_path("*")).map do |s| + File.basename(s) + end + end + end + end +end diff --git a/lib/omnibus-ctl/mixin/service_utils.rb b/lib/omnibus-ctl/mixin/service_utils.rb new file mode 100644 index 0000000..b9891bc --- /dev/null +++ b/lib/omnibus-ctl/mixin/service_utils.rb @@ -0,0 +1,82 @@ +module Omnibus + module Mixin + module ServiceUtils + def services_exist?(services) + services.each do |s| + return false unless ALL_SERVICES.include?(s) + end + true + end + + def recommend_services(services, set = ALL_SERVICES, description = "service") + bad_services = services.reject { |s| set.include?(s) } + bad_services.each do |s| + puts "Unknown #{description}: #{s}" + if rec = find_recommendation(s, set) + puts "\nDid you mean:" + puts " #{rec}" + end + end + end + + def recommend_params(params) + recommend_services(params, ALL_SERVICES + ALL_SYSPARAMS, "service or system component") + end + + def graceful_kill(service = nil, quiet = config[:quiet]) + services_to_kill = service.nil? ? ALL_SERVICES : Array(service) + services_to_kill.each do |svc| + graceful_kill_service(svc, quiet) + end + end + + def graceful_kill_service(service_name, quiet) + pidfile = "#{SV_PATH}/#{service_name}/supervise/pid" + pid = File.read(pidfile).chomp if File.exists?(pidfile) + pgrp = nil + + if pid.nil? || !is_integer?(pid) + log "could not find #{service_name} runit pidfile (service already stopped?), cannot attempt SIGKILL..." unless quiet + else + pgrp = get_pgrp_from_pid(pid) + end + + if pgrp.nil? || !is_integer?(pgrp) + log "could not find pgrp of pid #{pid} (not running?), cannot attempt SIGKILL..." unless quiet + end + + res = run_sv_command("stop", service_name, quiet: quiet) + if !pgrp.nil? + pids = get_pids_from_pgrp(pgrp) + unless pids.empty? + log "found stuck pids still running in process group: #{pids}, sending SIGKILL" unless quiet + sigkill_pgrp(pgrp) + end + else + res + end + end + + def is_integer?(string) + return true if Integer(string) rescue false + end + + def get_pgrp_from_pid(pid) + ps = `which ps`.chomp + `#{ps} -p #{pid} -o pgrp=`.chomp + end + + def get_pids_from_pgrp(pgrp) + pgrep = `which pgrep`.chomp + `#{pgrep} -g #{pgrp}`.split(/\n/).join(" ") + end + + def sigkill_pgrp(pgrp) + pkill = `which pkill`.chomp + run_command("#{pkill} -9 -g #{pgrp}") + end + + + end + end +end diff --git a/lib/omnibus-ctl/mixin/tcp_utils.rb b/lib/omnibus-ctl/mixin/tcp_utils.rb new file mode 100644 index 0000000..0aab900 --- /dev/null +++ b/lib/omnibus-ctl/mixin/tcp_utils.rb @@ -0,0 +1,24 @@ +require 'socket' +module Omnibus + module Mixin + module TcpUtils + + def wait_for_listening_port(wait_time, port, host = "127.0.0.1") + count = 0 + begin + s = Socket.tcp(host, port, nil, nil, connect_timeout: 1) + s.close + rescue Errno::ETIMEDOUT, Errno::ECONNRESET, + Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e + count += 1 + if count < wait_time + sleep 1 unless e.class == Errno::ETIMEDOUT + retry + else + raise e + end + end + end + end + end +end diff --git a/lib/omnibus-ctl/version.rb b/lib/omnibus-ctl/version.rb index c8bb1fc..36523fd 100644 --- a/lib/omnibus-ctl/version.rb +++ b/lib/omnibus-ctl/version.rb @@ -1,5 +1,5 @@ module Omnibus class Ctl - VERSION = "0.6.0" + VERSION = "1.0.0" end end diff --git a/omnibus-ctl.gemspec b/omnibus-ctl.gemspec index 4179b4a..04927ac 100644 --- a/omnibus-ctl.gemspec +++ b/omnibus-ctl.gemspec @@ -12,11 +12,22 @@ Gem::Specification.new do |s| s.summary = %q{Provides command line control for omnibus packages} s.description = %q{Provides command line control for omnibus pakcages, rarely used as a gem} + s.add_dependency "highline" + s.add_dependency "rest-client" +# s.add_dependency "pg", "= 0.17.1" # Locked to the version currently in omnibus-software + s.add_dependency "mixlib-config" + s.add_dependency "mixlib-cli" + s.add_dependency "mixlib-log" + s.add_dependency "levenshtein-ffi", "~> 1.1" + # specify any dependencies here; for example: s.add_development_dependency "rake" s.add_development_dependency "rspec", "~> 3.2" s.add_development_dependency "rspec_junit_formatter" - + s.add_development_dependency "bundler", "~> 1.7" + s.add_development_dependency "chefstyle" + s.add_development_dependency "guard-rspec" # check if we are still using this + s.bindir = "bin" s.executables = 'omnibus-ctl' s.require_path = 'lib' diff --git a/spec/command/base_spec.rb b/spec/command/base_spec.rb new file mode 100644 index 0000000..6b4a641 --- /dev/null +++ b/spec/command/base_spec.rb @@ -0,0 +1,4 @@ +require 'spec_helper' +require 'omnibus/ctl' +require 'omnibus/command/base' + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a4b3204..05fb71f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ # # Author: adam@opscode.com # -# Copyright 2012, Opscode, Inc. +# Copyright 2012-18, Opscode, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,8 +19,42 @@ require 'rspec' require 'omnibus-ctl' +require 'simplecov' +require 'stringio' + +SimpleCov.start do + track_files "lib/**/*.rb" + add_filter "spec/*.rb" +end + RSpec.configure do |config| - config.filter_run :focus => true + config.filter_run focus: true +# config.order = 'random' config.run_all_when_everything_filtered = true end +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) + +RSpec.shared_context "captured output" do + before do + # quiet Omnibus::Log + @old_log_level = Omnibus::Log.level + Omnibus::Log.level :fatal + @my_stdout = StringIO.new + @my_stderr = StringIO.new + @old_stdout = $stdout + @old_stderr = $stderr + $stdout = @my_stdout + $stderr = @my_stderr + @old_highline_use_color = HighLine.use_color? + end + + after do + Omnibus::Log.level @old_log_level + $stdout = @old_stdout + $stderr = @old_stderr + HighLine.use_color = @old_highline_use_color + end +end + +