module Chef::Knife::WinrmCommandSharedFunctions

Public Class Methods

included(includer) click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 35
      def self.included(includer)
        includer.class_eval do

          @@ssl_warning_given = false

          include Chef::Knife::WinrmBase
          include Chef::Knife::WinrmSharedOptions

          def validate_winrm_options!
            winrm_auth_protocol = config[:winrm_authentication_protocol]

            unless Chef::Knife::WinrmBase::WINRM_AUTH_PROTOCOL_LIST.include?(winrm_auth_protocol)
              ui.error "Invalid value '#{winrm_auth_protocol}' for --winrm-authentication-protocol option."
              ui.info "Valid values are #{Chef::Knife::WinrmBase::WINRM_AUTH_PROTOCOL_LIST.join(",")}."
              exit 1
            end

            warn_no_ssl_peer_verification if resolve_no_ssl_peer_verification
          end

          # Overrides Chef::Knife#configure_session, as that code is tied to the SSH implementation
          # Tracked by Issue # 3042 / https://github.com/chef/chef/issues/3042
          def configure_session
            validate_winrm_options!
            resolve_session_options
            resolve_target_nodes
            session_from_list
          end

          def resolve_target_nodes
            @list = case config[:manual]
                   when true
                     @name_args[0].split(" ")
                   when false
                     r = []
                     q = Chef::Search::Query.new
                     @action_nodes = q.search(:node, @name_args[0])[0]
                     @action_nodes.each do |item|
                       i = extract_nested_value(item, config[:attribute])
                       r.push(i) unless i.nil?
                     end
                     r
                   end

            if @list.length == 0
              if @action_nodes.length == 0
                ui.fatal("No nodes returned from search!")
              else
                ui.fatal("#{@action_nodes.length} #{@action_nodes.length > 1 ? "nodes" : "node"} found, " +
                         "but does not have the required attribute (#{config[:attribute]}) to establish the connection. " +
                         "Try setting another attribute to open the connection using --attribute.")
              end
              exit 10
           end
          end

          # TODO: Copied from Knife::Core:GenericPresenter. Should be extracted
          def extract_nested_value(data, nested_value_spec)
            nested_value_spec.split(".").each do |attr|
              if data.nil?
                nil # don't get no method error on nil
              elsif data.respond_to?(attr.to_sym)
                data = data.send(attr.to_sym)
              elsif data.respond_to?(:[])
                data = data[attr]
              else
                data = begin
                         data.send(attr.to_sym)
                       rescue NoMethodError
                         nil
                       end
              end
            end
            ( !data.is_a?(Array) && data.respond_to?(:to_hash) ) ? data.to_hash : data
          end

          def run_command(command = "")
            relay_winrm_command(command)
            check_for_errors!
            @exit_code
          end

          def relay_winrm_command(command)
            Chef::Log.debug(command)
            @session_results = []
            queue = Queue.new
            @winrm_sessions.each { |s| queue << s }
            num_sessions = config[:concurrency]
            num_targets = @winrm_sessions.length
            num_sessions = (num_sessions.nil? || num_sessions == 0) ? num_targets : [num_sessions, num_targets].min

            # These nils will kill the Threads once no more sessions are left
            num_sessions.times { queue << nil }
            threads = []
            num_sessions.times do
              threads << Thread.new do
                while session = queue.pop
                  run_command_in_thread(session, command)
                end
              end
            end
            threads.map(&:join)
            @session_results
          end

          private

          def run_command_in_thread(s, command)
            @session_results << s.relay_command(command)
          rescue WinRM::WinRMHTTPTransportError, WinRM::WinRMAuthorizationError => e
            if authorization_error?(e)
              unless config[:suppress_auth_failure]
                # Display errors if the caller hasn't opted to retry
                ui.error "Failed to authenticate to #{s.host} as #{config[:winrm_user]}"
                ui.info "Response: #{e.message}"
                ui.info get_failed_authentication_hint
                raise e
              end
            else
              raise e
            end
          end

          def get_failed_authentication_hint
            if @session_opts[:basic_auth_only]
              FAILED_BASIC_HINT
            else
              FAILED_NOT_BASIC_HINT
            end
          end

          def authorization_error?(exception)
            exception.is_a?(WinRM::WinRMAuthorizationError) ||
              exception.message =~ /401/
          end

          def check_for_errors!
            @exit_code ||= 0
            @winrm_sessions.each do |session|
              session_exit_code = session.exit_code
              unless success_return_codes.include? session_exit_code.to_i
                @exit_code = [@exit_code, session_exit_code.to_i].max
                ui.error "Failed to execute command on #{session.host} return code #{session_exit_code}"
              end
            end
          end

          def success_return_codes
            # Redundant if the CLI options parsing occurs
            return [0] unless config[:returns]

            @success_return_codes ||= config[:returns].split(",").collect(&:to_i)
          end

          def session_from_list
            @list.each do |item|
              Chef::Log.debug("Adding #{item}")
              @session_opts[:host] = item
              create_winrm_session(@session_opts)
            end
          end

          def create_winrm_session(options = {})
            session = Chef::Knife::WinrmSession.new(options)
            @winrm_sessions ||= []
            @winrm_sessions.push(session)
          end

          def resolve_session_options
            config[:winrm_port] ||= ( config[:winrm_transport] == "ssl" ) ? "5986" : "5985"

            @session_opts = {
              user: resolve_winrm_user,
              password: config[:winrm_password],
              port: config[:winrm_port],
              operation_timeout: resolve_winrm_session_timeout,
              basic_auth_only: resolve_winrm_basic_auth,
              disable_sspi: resolve_winrm_disable_sspi,
              transport: resolve_winrm_transport,
              no_ssl_peer_verification: resolve_no_ssl_peer_verification,
              ssl_peer_fingerprint: resolve_ssl_peer_fingerprint,
              shell: config[:winrm_shell],
              codepage: config[:winrm_codepage],
            }

            if @session_opts[:user] && (not @session_opts[:password])
              @session_opts[:password] = config[:winrm_password] = get_password
            end

            if @session_opts[:transport] == :kerberos
              @session_opts.merge!(resolve_winrm_kerberos_options)
            end

            @session_opts[:ca_trust_path] = config[:ca_trust_file] if config[:ca_trust_file]
          end

          def resolve_winrm_user
            user = config[:winrm_user]

            # Prefixing with '.\' when using negotiate
            # to auth user against local machine domain
            if resolve_winrm_basic_auth ||
                resolve_winrm_transport == :kerberos ||
                user.include?("\\") ||
                user.include?("@")
              user
            else
              ".\\#{user}"
            end
          end

          def resolve_winrm_session_timeout
            # 30 min (Default) OperationTimeout for long bootstraps fix for KNIFE_WINDOWS-8
            config[:session_timeout].to_i * 60 if config[:session_timeout]
          end

          def resolve_winrm_basic_auth
            config[:winrm_authentication_protocol] == "basic"
          end

          def resolve_winrm_kerberos_options
            kerberos_opts = {}
            kerberos_opts[:keytab] = config[:kerberos_keytab_file] if config[:kerberos_keytab_file]
            kerberos_opts[:realm] = config[:kerberos_realm] if config[:kerberos_realm]
            kerberos_opts[:service] = config[:kerberos_service] if config[:kerberos_service]
            kerberos_opts
          end

          def resolve_winrm_transport
            transport = config[:winrm_transport].to_sym
            if config.any? { |k, v| k.to_s =~ /kerberos/ && !v.nil? }
              transport = :kerberos
            elsif transport != :ssl && negotiate_auth?
              transport = :negotiate
            end

            transport
          end

          def resolve_no_ssl_peer_verification
            config[:ca_trust_file].nil? && config[:winrm_ssl_verify_mode] == :verify_none && resolve_winrm_transport == :ssl
          end

          def resolve_ssl_peer_fingerprint
            config[:ssl_peer_fingerprint]
          end

          def resolve_winrm_disable_sspi
            resolve_winrm_transport != :negotiate
          end

          def get_password
            @password ||= ui.ask("Enter your password: ") { |q| q.echo = false }
          end

          def negotiate_auth?
            config[:winrm_authentication_protocol] == "negotiate"
          end

          def warn_no_ssl_peer_verification
            unless @@ssl_warning_given
              @@ssl_warning_given = true
              ui.warn(<<~WARN)
                * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
                SSL validation of HTTPS requests for the WinRM transport is disabled. HTTPS WinRM
                connections are still encrypted, but knife is not able to detect forged replies
                or spoofing attacks.

                To fix this issue add an entry like this to your knife configuration file:

                ```
                  # Verify all WinRM HTTPS connections (default, recommended)
                  knife[:winrm_ssl_verify_mode] = :verify_peer
                ```
                * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
              WARN
                end
              end

        end
      end

Public Instance Methods

authorization_error?(exception) click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 166
def authorization_error?(exception)
  exception.is_a?(WinRM::WinRMAuthorizationError) ||
    exception.message =~ /401/
end
check_for_errors!() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 171
def check_for_errors!
  @exit_code ||= 0
  @winrm_sessions.each do |session|
    session_exit_code = session.exit_code
    unless success_return_codes.include? session_exit_code.to_i
      @exit_code = [@exit_code, session_exit_code.to_i].max
      ui.error "Failed to execute command on #{session.host} return code #{session_exit_code}"
    end
  end
end
configure_session() click to toggle source

Overrides Chef::Knife#configure_session, as that code is tied to the SSH implementation Tracked by Issue # 3042 / github.com/chef/chef/issues/3042

# File lib/chef/knife/helpers/winrm_knife_base.rb, line 57
def configure_session
  validate_winrm_options!
  resolve_session_options
  resolve_target_nodes
  session_from_list
end
create_winrm_session(options = {}) click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 197
def create_winrm_session(options = {})
  session = Chef::Knife::WinrmSession.new(options)
  @winrm_sessions ||= []
  @winrm_sessions.push(session)
end
extract_nested_value(data, nested_value_spec) click to toggle source

TODO: Copied from Knife::Core:GenericPresenter. Should be extracted

# File lib/chef/knife/helpers/winrm_knife_base.rb, line 92
def extract_nested_value(data, nested_value_spec)
  nested_value_spec.split(".").each do |attr|
    if data.nil?
      nil # don't get no method error on nil
    elsif data.respond_to?(attr.to_sym)
      data = data.send(attr.to_sym)
    elsif data.respond_to?(:[])
      data = data[attr]
    else
      data = begin
               data.send(attr.to_sym)
             rescue NoMethodError
               nil
             end
    end
  end
  ( !data.is_a?(Array) && data.respond_to?(:to_hash) ) ? data.to_hash : data
end
get_failed_authentication_hint() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 158
def get_failed_authentication_hint
  if @session_opts[:basic_auth_only]
    FAILED_BASIC_HINT
  else
    FAILED_NOT_BASIC_HINT
  end
end
get_password() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 286
def get_password
  @password ||= ui.ask("Enter your password: ") { |q| q.echo = false }
end
negotiate_auth?() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 290
def negotiate_auth?
  config[:winrm_authentication_protocol] == "negotiate"
end
relay_winrm_command(command) click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 117
def relay_winrm_command(command)
  Chef::Log.debug(command)
  @session_results = []
  queue = Queue.new
  @winrm_sessions.each { |s| queue << s }
  num_sessions = config[:concurrency]
  num_targets = @winrm_sessions.length
  num_sessions = (num_sessions.nil? || num_sessions == 0) ? num_targets : [num_sessions, num_targets].min

  # These nils will kill the Threads once no more sessions are left
  num_sessions.times { queue << nil }
  threads = []
  num_sessions.times do
    threads << Thread.new do
      while session = queue.pop
        run_command_in_thread(session, command)
      end
    end
  end
  threads.map(&:join)
  @session_results
end
resolve_no_ssl_peer_verification() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 274
def resolve_no_ssl_peer_verification
  config[:ca_trust_file].nil? && config[:winrm_ssl_verify_mode] == :verify_none && resolve_winrm_transport == :ssl
end
resolve_session_options() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 203
def resolve_session_options
  config[:winrm_port] ||= ( config[:winrm_transport] == "ssl" ) ? "5986" : "5985"

  @session_opts = {
    user: resolve_winrm_user,
    password: config[:winrm_password],
    port: config[:winrm_port],
    operation_timeout: resolve_winrm_session_timeout,
    basic_auth_only: resolve_winrm_basic_auth,
    disable_sspi: resolve_winrm_disable_sspi,
    transport: resolve_winrm_transport,
    no_ssl_peer_verification: resolve_no_ssl_peer_verification,
    ssl_peer_fingerprint: resolve_ssl_peer_fingerprint,
    shell: config[:winrm_shell],
    codepage: config[:winrm_codepage],
  }

  if @session_opts[:user] && (not @session_opts[:password])
    @session_opts[:password] = config[:winrm_password] = get_password
  end

  if @session_opts[:transport] == :kerberos
    @session_opts.merge!(resolve_winrm_kerberos_options)
  end

  @session_opts[:ca_trust_path] = config[:ca_trust_file] if config[:ca_trust_file]
end
resolve_ssl_peer_fingerprint() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 278
def resolve_ssl_peer_fingerprint
  config[:ssl_peer_fingerprint]
end
resolve_target_nodes() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 64
def resolve_target_nodes
  @list = case config[:manual]
         when true
           @name_args[0].split(" ")
         when false
           r = []
           q = Chef::Search::Query.new
           @action_nodes = q.search(:node, @name_args[0])[0]
           @action_nodes.each do |item|
             i = extract_nested_value(item, config[:attribute])
             r.push(i) unless i.nil?
           end
           r
         end

  if @list.length == 0
    if @action_nodes.length == 0
      ui.fatal("No nodes returned from search!")
    else
      ui.fatal("#{@action_nodes.length} #{@action_nodes.length > 1 ? "nodes" : "node"} found, " +
               "but does not have the required attribute (#{config[:attribute]}) to establish the connection. " +
               "Try setting another attribute to open the connection using --attribute.")
    end
    exit 10
 end
end
resolve_winrm_basic_auth() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 251
def resolve_winrm_basic_auth
  config[:winrm_authentication_protocol] == "basic"
end
resolve_winrm_disable_sspi() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 282
def resolve_winrm_disable_sspi
  resolve_winrm_transport != :negotiate
end
resolve_winrm_kerberos_options() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 255
def resolve_winrm_kerberos_options
  kerberos_opts = {}
  kerberos_opts[:keytab] = config[:kerberos_keytab_file] if config[:kerberos_keytab_file]
  kerberos_opts[:realm] = config[:kerberos_realm] if config[:kerberos_realm]
  kerberos_opts[:service] = config[:kerberos_service] if config[:kerberos_service]
  kerberos_opts
end
resolve_winrm_session_timeout() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 246
def resolve_winrm_session_timeout
  # 30 min (Default) OperationTimeout for long bootstraps fix for KNIFE_WINDOWS-8
  config[:session_timeout].to_i * 60 if config[:session_timeout]
end
resolve_winrm_transport() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 263
def resolve_winrm_transport
  transport = config[:winrm_transport].to_sym
  if config.any? { |k, v| k.to_s =~ /kerberos/ && !v.nil? }
    transport = :kerberos
  elsif transport != :ssl && negotiate_auth?
    transport = :negotiate
  end

  transport
end
resolve_winrm_user() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 231
def resolve_winrm_user
  user = config[:winrm_user]

  # Prefixing with '.\' when using negotiate
  # to auth user against local machine domain
  if resolve_winrm_basic_auth ||
      resolve_winrm_transport == :kerberos ||
      user.include?("\\") ||
      user.include?("@")
    user
  else
    ".\\#{user}"
  end
end
run_command(command = "") click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 111
def run_command(command = "")
  relay_winrm_command(command)
  check_for_errors!
  @exit_code
end
run_command_in_thread(s, command) click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 142
def run_command_in_thread(s, command)
  @session_results << s.relay_command(command)
rescue WinRM::WinRMHTTPTransportError, WinRM::WinRMAuthorizationError => e
  if authorization_error?(e)
    unless config[:suppress_auth_failure]
      # Display errors if the caller hasn't opted to retry
      ui.error "Failed to authenticate to #{s.host} as #{config[:winrm_user]}"
      ui.info "Response: #{e.message}"
      ui.info get_failed_authentication_hint
      raise e
    end
  else
    raise e
  end
end
session_from_list() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 189
def session_from_list
  @list.each do |item|
    Chef::Log.debug("Adding #{item}")
    @session_opts[:host] = item
    create_winrm_session(@session_opts)
  end
end
success_return_codes() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 182
def success_return_codes
  # Redundant if the CLI options parsing occurs
  return [0] unless config[:returns]

  @success_return_codes ||= config[:returns].split(",").collect(&:to_i)
end
validate_winrm_options!() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 43
def validate_winrm_options!
  winrm_auth_protocol = config[:winrm_authentication_protocol]

  unless Chef::Knife::WinrmBase::WINRM_AUTH_PROTOCOL_LIST.include?(winrm_auth_protocol)
    ui.error "Invalid value '#{winrm_auth_protocol}' for --winrm-authentication-protocol option."
    ui.info "Valid values are #{Chef::Knife::WinrmBase::WINRM_AUTH_PROTOCOL_LIST.join(",")}."
    exit 1
  end

  warn_no_ssl_peer_verification if resolve_no_ssl_peer_verification
end
warn_no_ssl_peer_verification() click to toggle source
# File lib/chef/knife/helpers/winrm_knife_base.rb, line 294
          def warn_no_ssl_peer_verification
            unless @@ssl_warning_given
              @@ssl_warning_given = true
              ui.warn(<<~WARN)
                * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
                SSL validation of HTTPS requests for the WinRM transport is disabled. HTTPS WinRM
                connections are still encrypted, but knife is not able to detect forged replies
                or spoofing attacks.

                To fix this issue add an entry like this to your knife configuration file:

                ```
                  # Verify all WinRM HTTPS connections (default, recommended)
                  knife[:winrm_ssl_verify_mode] = :verify_peer
                ```
                * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
              WARN
                end
              end