class Chef::Knife

Constants

OFFICIAL_PLUGINS

Attributes

name_args[RW]
ui[RW]

Public Class Methods

category(new_category) click to toggle source

Explicitly set the category for the current command to new_category The category is normally determined from the first word of the command name, but some commands make more sense using two or more words @param new_category [String] value to set the category to (see examples)

@example Data bag commands would be in the 'data' category by default. To

put them in the 'data bag' category:
category('data bag')
# File lib/chef/knife.rb, line 115
def self.category(new_category)
  @category = new_category
end
chef_config_dir() click to toggle source
# File lib/chef/knife.rb, line 195
def self.chef_config_dir
  @@chef_config_dir ||= config_loader.chef_config_dir
end
common_name() click to toggle source
# File lib/chef/knife.rb, line 127
def self.common_name
  snake_case_name.split("_").join(" ")
end
config_loader() click to toggle source
# File lib/chef/knife.rb, line 178
def self.config_loader
  @config_loader ||= WorkstationConfigLoader.new(nil, Chef::Log)
end
dependency_loaders() click to toggle source
# File lib/chef/knife.rb, line 225
def self.dependency_loaders
  @dependency_loaders ||= []
end
deps(&block) click to toggle source
# File lib/chef/knife.rb, line 229
def self.deps(&block)
  dependency_loaders << block
end
guess_category(args) click to toggle source
# File lib/chef/knife.rb, line 144
def self.guess_category(args)
  subcommand_loader.guess_category(args)
end
inherited(subclass) click to toggle source
Calls superclass method
# File lib/chef/knife.rb, line 90
def self.inherited(subclass)
  super
  unless subclass.unnamed?
    subcommands[subclass.snake_case_name] = subclass
    subcommand_files[subclass.snake_case_name] +=
      if subclass.superclass.to_s == "Chef::ChefFS::Knife"
        # ChefFS-based commands have a superclass that defines an
        # inhereited method which calls super. This means that the
        # top of the call stack is not the class definition for
        # our subcommand.  Try the second entry in the call stack.
        [path_from_caller(caller[1])]
      else
        [path_from_caller(caller[0])]
      end
  end
end
list_commands(preferred_category = nil) click to toggle source
# File lib/chef/knife.rb, line 242
def list_commands(preferred_category = nil)
  category_desc = preferred_category ? preferred_category + " " : ""
  msg "Available #{category_desc}subcommands: (for details, knife SUB-COMMAND --help)\n\n"
  subcommand_loader.list_commands(preferred_category).sort.each do |category, commands|
    next if category =~ /deprecated/i
    msg "** #{category.upcase} COMMANDS **"
    commands.sort.each do |command|
      subcommand_loader.load_command(command)
      msg subcommands[command].banner if subcommands[command]
    end
    msg
  end
end
load_commands() click to toggle source
# File lib/chef/knife.rb, line 140
def self.load_commands
  @commands_loaded ||= subcommand_loader.load_commands
end
load_config(explicit_config_file, profile) click to toggle source
# File lib/chef/knife.rb, line 182
def self.load_config(explicit_config_file, profile)
  config_loader.explicit_config_file = explicit_config_file
  config_loader.profile = profile
  config_loader.load

  ui.warn("No knife configuration file found. See https://docs.chef.io/config_rb_knife.html for details.") if config_loader.no_config_found?

  config_loader
rescue Exceptions::ConfigurationError => e
  ui.error(ui.color("CONFIGURATION ERROR:", :red) + e.message)
  exit 1
end
load_deps() click to toggle source
# File lib/chef/knife.rb, line 233
def self.load_deps
  dependency_loaders.each do |dep_loader|
    dep_loader.call
  end
end
msg(msg = "") click to toggle source
# File lib/chef/knife.rb, line 76
def self.msg(msg = "")
  ui.msg(msg)
end
new(argv = []) click to toggle source

Create a new instance of the current class configured for the given arguments and options

Calls superclass method
# File lib/chef/knife.rb, line 296
def initialize(argv = [])
  super() # having to call super in initialize is the most annoying anti-pattern :(
  @ui = Chef::Knife::UI.new(STDOUT, STDERR, STDIN, config)

  command_name_words = self.class.snake_case_name.split("_")

  # Mixlib::CLI ignores the embedded name_args
  @name_args = parse_options(argv)
  @name_args.delete(command_name_words.join("-"))
  @name_args.reject! { |name_arg| command_name_words.delete(name_arg) }

  # knife node run_list add requires that we have extra logic to handle
  # the case that command name words could be joined by an underscore :/
  command_name_joined = command_name_words.join("_")
  @name_args.reject! { |name_arg| command_name_joined == name_arg }

  # Similar handling for hyphens.
  command_name_joined = command_name_words.join("-")
  @name_args.reject! { |name_arg| command_name_joined == name_arg }

  if config[:help]
    msg opt_parser
    exit 1
  end

  # Grab a copy before config merge occurs, so that we can later identify
  # whare a given config value is sourced from.
  @original_config = config.dup

  # copy Mixlib::CLI over so that it can be configured in config.rb/knife.rb
  # config file
  Chef::Config[:verbosity] = config[:verbosity] if config[:verbosity]
end
reset_config_loader!() click to toggle source
# File lib/chef/knife.rb, line 80
def self.reset_config_loader!
  @@chef_config_dir = nil
  @config_loader = nil
end
reset_subcommands!() click to toggle source
# File lib/chef/knife.rb, line 85
def self.reset_subcommands!
  @@subcommands = {}
  @subcommands_by_category = nil
end
run(args, options = {}) click to toggle source

Run knife for the given args (ARGV), adding options to the list of CLI options that the subcommand knows how to handle.

@param args [Array] The arguments. Usually ARGV @param options [Mixlib::CLI option parser hash] These options are how

subcommands know about global knife CLI options
# File lib/chef/knife.rb, line 206
def self.run(args, options = {})
  # Fallback debug logging. Normally the logger isn't configured until we
  # read the config, but this means any logging that happens before the
  # config file is read may be lost. If the KNIFE_DEBUG variable is set, we
  # setup the logger for debug logging to stderr immediately to catch info
  # from early in the setup process.
  if ENV["KNIFE_DEBUG"]
    Chef::Log.init($stderr)
    Chef::Log.level(:debug)
  end

  subcommand_class = subcommand_class_from(args)
  subcommand_class.options = options.merge!(subcommand_class.options)
  subcommand_class.load_deps
  instance = subcommand_class.new(args)
  instance.configure_chef
  instance.run_with_pretty_exceptions
end
snake_case_name() click to toggle source
# File lib/chef/knife.rb, line 123
def self.snake_case_name
  convert_to_snake_case(name.split("::").last) unless unnamed?
end
subcommand_category() click to toggle source
# File lib/chef/knife.rb, line 119
def self.subcommand_category
  @category || snake_case_name.split("_").first unless unnamed?
end
subcommand_class_from(args) click to toggle source
# File lib/chef/knife.rb, line 148
def self.subcommand_class_from(args)
  if args.size == 1 && args[0].strip.casecmp("rehash") == 0
    # To prevent issues with the rehash file not pointing to the correct plugins,
    # we always use the glob loader when regenerating the rehash file
    @subcommand_loader = Chef::Knife::SubcommandLoader.gem_glob_loader(chef_config_dir)
  end
  subcommand_loader.command_class_from(args) || subcommand_not_found!(args)
end
subcommand_files() click to toggle source
# File lib/chef/knife.rb, line 161
def self.subcommand_files
  @@subcommand_files ||= Hash.new([])
end
subcommand_loader() click to toggle source
# File lib/chef/knife.rb, line 136
def self.subcommand_loader
  @subcommand_loader ||= Chef::Knife::SubcommandLoader.for_config(chef_config_dir)
end
subcommands() click to toggle source
# File lib/chef/knife.rb, line 157
def self.subcommands
  @@subcommands ||= {}
end
subcommands_by_category() click to toggle source
# File lib/chef/knife.rb, line 165
def self.subcommands_by_category
  unless @subcommands_by_category
    @subcommands_by_category = Hash.new { |hash, key| hash[key] = [] }
    subcommands.each do |snake_cased, klass|
      @subcommands_by_category[klass.subcommand_category] << snake_cased
    end
  end
  @subcommands_by_category
end
ui() click to toggle source
# File lib/chef/knife.rb, line 72
def self.ui
  @ui ||= Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
end
unnamed?() click to toggle source

Does this class have a name? (Classes created via Class.new don't)

# File lib/chef/knife.rb, line 132
def self.unnamed?
  name.nil? || name.empty?
end
use_separate_defaults?() click to toggle source

Configure mixlib-cli to always separate defaults from user-supplied CLI options

# File lib/chef/knife.rb, line 68
def self.use_separate_defaults?
  true
end

Private Class Methods

path_from_caller(caller_line) click to toggle source

@api private

# File lib/chef/knife.rb, line 259
def path_from_caller(caller_line)
  caller_line.split(/:\d+/).first
end
reset_config_path!() click to toggle source

@api private

# File lib/chef/knife.rb, line 286
def reset_config_path!
  @@chef_config_dir = nil
end
subcommand_not_found!(args) click to toggle source

Error out and print usage. probably because the arguments given by the user could not be resolved to a subcommand. @api private

# File lib/chef/knife.rb, line 266
def subcommand_not_found!(args)
  ui.fatal("Cannot find subcommand for: '#{args.join(' ')}'")

  # Mention rehash when the subcommands cache(plugin_manifest.json) is used
  if subcommand_loader.is_a?(Chef::Knife::SubcommandLoader::HashedCommandLoader)
    ui.info("If this is a recently installed plugin, please run 'knife rehash' to update the subcommands cache.")
  end

  if category_commands = guess_category(args)
    list_commands(category_commands)
  elsif OFFICIAL_PLUGINS.include?(args[0]) # command was an uninstalled official chef knife plugin
    ui.info("Use `#{Chef::Dist::EXEC} gem install knife-#{args[0]}` to install the plugin into ChefDK")
  else
    list_commands
  end

  exit 10
end

Public Instance Methods

api_key() click to toggle source
# File lib/chef/knife.rb, line 562
def api_key
  Chef::Config[:client_key]
end
apply_computed_config() click to toggle source

Catch-all method that does any massaging needed for various config components, such as expanding file paths and converting verbosity level into log level.

# File lib/chef/knife.rb, line 395
def apply_computed_config
  Chef::Config[:color] = config[:color]

  case Chef::Config[:verbosity]
  when 0, nil
    Chef::Config[:log_level] = :warn
  when 1
    Chef::Config[:log_level] = :info
  when 2
    Chef::Config[:log_level] = :debug
  else
    Chef::Config[:log_level] = :trace
  end

  Chef::Config[:log_level] = :trace if ENV["KNIFE_DEBUG"]

  Chef::Config[:node_name]         = config[:node_name]       if config[:node_name]
  Chef::Config[:client_key]        = config[:client_key]      if config[:client_key]
  Chef::Config[:chef_server_url]   = config[:chef_server_url] if config[:chef_server_url]
  Chef::Config[:environment]       = config[:environment]     if config[:environment]

  Chef::Config.local_mode = config[:local_mode] if config.key?(:local_mode)

  Chef::Config.listen = config[:listen] if config.key?(:listen)

  if Chef::Config.local_mode && !Chef::Config.key?(:cookbook_path) && !Chef::Config.key?(:chef_repo_path)
    Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd)
  end
  Chef::Config.chef_zero.host = config[:chef_zero_host] if config[:chef_zero_host]
  Chef::Config.chef_zero.port = config[:chef_zero_port] if config[:chef_zero_port]

  # Expand a relative path from the config directory. Config from command
  # line should already be expanded, and absolute paths will be unchanged.
  if Chef::Config[:client_key] && config[:config_file]
    Chef::Config[:client_key] = File.expand_path(Chef::Config[:client_key], File.dirname(config[:config_file]))
  end

  Mixlib::Log::Formatter.show_time = false
  Chef::Log.init(Chef::Config[:log_location])
  Chef::Log.level(Chef::Config[:log_level] || :error)
end
cli_keys() click to toggle source

keys from mixlib-cli options

# File lib/chef/knife.rb, line 339
def cli_keys
  self.class.options.keys
end
config_file_settings() click to toggle source

extracts the settings from the Chef::Config sub-hash that correspond to knife cli options – in preparation for merging config values with cli values

NOTE: due to weirdness in mixlib-config has_key? is only true if the value has been set by the user – the Chef::Config defaults return has_key?() of false and this code DEPENDS on that functionality since applying the default values in Chef::Config would break the defaults in the cli that we would otherwise overwrite.

# File lib/chef/knife.rb, line 351
def config_file_settings
  cli_keys.each_with_object({}) do |key, memo|
    if Chef::Config[:knife].key?(key)
      memo[key] = Chef::Config[:knife][key]
    end
  end
end
config_source(key) click to toggle source

Determine the source of a given configuration key

@argument key [Symbol] a configuration key @return [Symbol,NilClass] return the source of the config key, one of:

- :cli - this was explicitly provided on the CLI
- :config - this came from Chef::Config[:knife]
- :cli_default - came from a declared CLI `option`'s `default` value.
- nil - if the key could not be found in any source.
        This can happen when it is invalid, or has been
        set directly into #config without then calling #merge_config
# File lib/chef/knife.rb, line 385
def config_source(key)
  return :cli if @original_config.include? key
  return :config if config_file_settings.key? key
  return :cli_default if default_config.include? key
  nil
end
configure_chef() click to toggle source
# File lib/chef/knife.rb, line 437
def configure_chef
  # knife needs to send logger output to STDERR by default
  Chef::Config[:log_location] = STDERR
  config_loader = self.class.load_config(config[:config_file], config[:profile])
  config[:config_file] = config_loader.config_location

  # For CLI options like `--config-option key=value`. These have to get
  # parsed and applied separately.
  extra_config_options = config.delete(:config_option)

  merge_configs
  apply_computed_config

  # This has to be after apply_computed_config so that Mixlib::Log is configured
  Chef::Log.info("Using configuration from #{config[:config_file]}") if config[:config_file]

  begin
    Chef::Config.apply_extra_config_options(extra_config_options)
  rescue ChefConfig::UnparsableConfigOption => e
    ui.error e.message
    show_usage
    exit(1)
  end

  Chef::Config.export_proxies
end
create_object(object, pretty_name = nil, object_class: nil) { |output| ... } click to toggle source

FIXME: yard with @yield

# File lib/chef/knife.rb, line 577
def create_object(object, pretty_name = nil, object_class: nil)
  output = if object_class
             edit_data(object, object_class: object_class)
           else
             edit_hash(object)
           end

  if Kernel.block_given?
    output = yield(output)
  else
    output.save
  end

  pretty_name ||= output

  msg("Created #{pretty_name}")

  output(output) if config[:print_after]
end
delete_object(klass, name, delete_name = nil) { || ... } click to toggle source

FIXME: yard with @yield

# File lib/chef/knife.rb, line 598
def delete_object(klass, name, delete_name = nil)
  confirm("Do you really want to delete #{name}")

  if Kernel.block_given?
    object = yield
  else
    object = klass.load(name)
    object.destroy
  end

  output(format_for_display(object)) if config[:print_after]

  obj_name = delete_name ? "#{delete_name}[#{name}]" : object
  msg("Deleted #{obj_name}")
end
format_rest_error(response) click to toggle source

Parses JSON from the error response sent by Chef Server and returns the error message

# File lib/chef/knife.rb, line 570
def format_rest_error(response)
  Array(Chef::JSONCompat.from_json(response.body)["error"]).join("; ")
rescue Exception
  response.body
end
humanize_exception(e) click to toggle source
# File lib/chef/knife.rb, line 483
def humanize_exception(e)
  case e
  when SystemExit
    raise # make sure exit passes through.
  when Net::HTTPClientException, Net::HTTPFatalError
    humanize_http_exception(e)
  when OpenSSL::SSL::SSLError
    ui.error "Could not establish a secure connection to the server."
    ui.info "Use `knife ssl check` to troubleshoot your SSL configuration."
    ui.info "If your server uses a self-signed certificate, you can use"
    ui.info "`knife ssl fetch` to make knife trust the server's certificates."
    ui.info ""
    ui.info  "Original Exception: #{e.class.name}: #{e.message}"
  when Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError
    ui.error "Network Error: #{e.message}"
    ui.info "Check your knife configuration and network settings"
  when NameError, NoMethodError
    ui.error "knife encountered an unexpected error"
    ui.info  "This may be a bug in the '#{self.class.common_name}' knife command or plugin"
    ui.info  "Please collect the output of this command with the `-VVV` option before filing a bug report."
    ui.info  "Exception: #{e.class.name}: #{e.message}"
  when Chef::Exceptions::PrivateKeyMissing
    ui.error "Your private key could not be loaded from #{api_key}"
    ui.info  "Check your configuration file and ensure that your private key is readable"
  when Chef::Exceptions::InvalidRedirect
    ui.error "Invalid Redirect: #{e.message}"
    ui.info  "Change your server location in config.rb/knife.rb to the server's FQDN to avoid unwanted redirections."
  else
    ui.error "#{e.class.name}: #{e.message}"
  end
end
humanize_http_exception(e) click to toggle source
# File lib/chef/knife.rb, line 515
def humanize_http_exception(e)
  response = e.response
  case response
  when Net::HTTPUnauthorized
    ui.error "Failed to authenticate to #{server_url} as #{username} with key #{api_key}"
    ui.info "Response:  #{format_rest_error(response)}"
  when Net::HTTPForbidden
    ui.error "You authenticated successfully to #{server_url} as #{username} but you are not authorized for this action."
    proxy_env_vars = ENV.to_hash.keys.map(&:downcase) & %w{http_proxy https_proxy ftp_proxy socks_proxy no_proxy}
    unless proxy_env_vars.empty?
      ui.error "There are proxy servers configured, your server url may need to be added to NO_PROXY."
    end
    ui.info "Response:  #{format_rest_error(response)}"
  when Net::HTTPBadRequest
    ui.error "The data in your request was invalid"
    ui.info "Response: #{format_rest_error(response)}"
  when Net::HTTPNotFound
    ui.error "The object you are looking for could not be found"
    ui.info "Response: #{format_rest_error(response)}"
  when Net::HTTPInternalServerError
    ui.error "internal server error"
    ui.info "Response: #{format_rest_error(response)}"
  when Net::HTTPBadGateway
    ui.error "bad gateway"
    ui.info "Response: #{format_rest_error(response)}"
  when Net::HTTPServiceUnavailable
    ui.error "Service temporarily unavailable"
    ui.info "Response: #{format_rest_error(response)}"
  when Net::HTTPNotAcceptable
    version_header = Chef::JSONCompat.from_json(response["x-ops-server-api-version"])
    client_api_version = version_header["request_version"]
    min_server_version = version_header["min_version"]
    max_server_version = version_header["max_version"]
    ui.error "The API version that Knife is using is not supported by the server you sent this request to."
    ui.info "The request that Knife sent was using API version #{client_api_version}."
    ui.info "The server you sent the request to supports a min API verson of #{min_server_version} and a max API version of #{max_server_version}."
    ui.info "Please either update your #{Chef::Dist::PRODUCT} or the server to be a compatible set."
  else
    ui.error response.message
    ui.info "Response: #{format_rest_error(response)}"
  end
end
maybe_setup_fips() click to toggle source
# File lib/chef/knife.rb, line 642
def maybe_setup_fips
  if !config[:fips].nil?
    Chef::Config[:fips] = config[:fips]
  end
  Chef::Config.init_openssl
end
merge_configs() click to toggle source

config is merged in this order (inverse of precedence)

default_config       - mixlib-cli defaults (accessor from the mixin)
config_file_settings - Chef::Config[:knife] sub-hash
config               - mixlib-cli settings (accessor from the mixin)
# File lib/chef/knife.rb, line 363
def merge_configs
  # Update our original_config - if someone has created a knife command
  # instance directly, they are likely ot have set cmd.config values directly
  # as well, at which point our saved original config is no longer up to date.
  @original_config = config.dup
  # other code may have a handle to the config object, so use Hash#replace to deliberately
  # update-in-place.
  config.replace(default_config.merge(config_file_settings).merge(config))
end
noauth_rest() click to toggle source
# File lib/chef/knife.rb, line 631
def noauth_rest
  @rest ||= begin
    require_relative "http/simple_json"
    Chef::HTTP::SimpleJSON.new(Chef::Config[:chef_server_url])
  end
end
parse_options(args) click to toggle source
Calls superclass method
# File lib/chef/knife.rb, line 330
def parse_options(args)
  super
rescue OptionParser::InvalidOption => e
  puts "Error: " + e.to_s
  show_usage
  exit(1)
end
rest() click to toggle source
# File lib/chef/knife.rb, line 624
def rest
  @rest ||= begin
    require_relative "server_api"
    Chef::ServerAPI.new(Chef::Config[:chef_server_url])
  end
end
run_with_pretty_exceptions(raise_exception = false) click to toggle source
# File lib/chef/knife.rb, line 468
def run_with_pretty_exceptions(raise_exception = false)
  unless respond_to?(:run)
    ui.error "You need to add a #run method to your knife command before you can use it"
  end
  enforce_path_sanity
  maybe_setup_fips
  Chef::LocalMode.with_server_connectivity do
    run
  end
rescue Exception => e
  raise if raise_exception || ( Chef::Config[:verbosity] && Chef::Config[:verbosity] >= 2 )
  humanize_exception(e)
  exit 100
end
server_url() click to toggle source
# File lib/chef/knife.rb, line 638
def server_url
  Chef::Config[:chef_server_url]
end
show_usage() click to toggle source
# File lib/chef/knife.rb, line 464
def show_usage
  stdout.puts("USAGE: " + opt_parser.to_s)
end
test_mandatory_field(field, fieldname) click to toggle source

helper method for testing if a field exists and returning the usage and proper error if not

# File lib/chef/knife.rb, line 616
def test_mandatory_field(field, fieldname)
  if field.nil?
    show_usage
    ui.fatal("You must specify a #{fieldname}")
    exit 1
  end
end
username() click to toggle source
# File lib/chef/knife.rb, line 558
def username
  Chef::Config[:node_name]
end