module Chef::Knife::WinrmCommandCommon

Public Class Methods

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

          @@ssl_warning_given = false

          include Chef::Knife::WinrmCore
          include Chef::Knife::WinrmOptions
          include Chef::Knife::KnifeWindowsCore

          def validate_winrm_options!
            winrm_auth_protocol = locate_config_value(:winrm_authentication_protocol)

            if ! Chef::Knife::WinrmCore::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::WinrmCore::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 = Array.new
                     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.kind_of?(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 }
            # These nils will kill the Threads once no more sessions are left
            locate_config_value(:concurrency).times { queue << nil }

            threads = []
            locate_config_value(:concurrency).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)
              if ! config[:suppress_auth_failure]
                # Display errors if the caller hasn't opted to retry
                ui.error "Failed to authenticate to #{s.host} as #{locate_config_value(: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]
            return @success_return_codes ||= config[:returns].split(',').collect {|item| item.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
            @session_opts = {
              user: resolve_winrm_user,
              password: locate_config_value(:winrm_password),
              port: locate_config_value(: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: locate_config_value(:winrm_shell),
              codepage: locate_config_value(:winrm_codepage)
            }

            if @session_opts[:user] and (not @session_opts[:password])
              @session_opts[:password] = Chef::Config[:knife][:winrm_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] = locate_config_value(:ca_trust_file) if locate_config_value(:ca_trust_file)
          end

          def resolve_winrm_user
            user = locate_config_value(: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
            locate_config_value(:session_timeout).to_i * 60 if locate_config_value(:session_timeout)
          end

          def resolve_winrm_basic_auth
            locate_config_value(:winrm_authentication_protocol) == "basic"
          end

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

          def resolve_winrm_transport
            transport = locate_config_value(: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
            locate_config_value(:ca_trust_file).nil? && config[:winrm_ssl_verify_mode] == :verify_none && resolve_winrm_transport == :ssl
          end

          def resolve_ssl_peer_fingerprint
            locate_config_value(: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?
            locate_config_value(:winrm_authentication_protocol) == "negotiate"
          end

          def warn_no_ssl_peer_verification
            if ! @@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/winops_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/winops_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/winops_winrm_knife_base.rb, line 59
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/winops_winrm_knife_base.rb, line 196
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/winops_winrm_knife_base.rb, line 94
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.kind_of?(Array) && data.respond_to?(:to_hash) ) ? data.to_hash : data
end
get_failed_authentication_hint() click to toggle source
# File lib/chef/knife/winops_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/winops_winrm_knife_base.rb, line 283
def get_password
  @password ||= ui.ask("Enter your password: ") { |q| q.echo = false }
end
negotiate_auth?() click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 287
def negotiate_auth?
  locate_config_value(:winrm_authentication_protocol) == "negotiate"
end
relay_winrm_command(command) click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 119
def relay_winrm_command(command)
  Chef::Log.debug(command)
  @session_results = []

  queue = Queue.new
  @winrm_sessions.each { |s| queue << s }
  # These nils will kill the Threads once no more sessions are left
  locate_config_value(:concurrency).times { queue << nil }

  threads = []
  locate_config_value(:concurrency).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/winops_winrm_knife_base.rb, line 271
def resolve_no_ssl_peer_verification
  locate_config_value(: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/winops_winrm_knife_base.rb, line 202
def resolve_session_options
  @session_opts = {
    user: resolve_winrm_user,
    password: locate_config_value(:winrm_password),
    port: locate_config_value(: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: locate_config_value(:winrm_shell),
    codepage: locate_config_value(:winrm_codepage)
  }

  if @session_opts[:user] and (not @session_opts[:password])
    @session_opts[:password] = Chef::Config[:knife][:winrm_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] = locate_config_value(:ca_trust_file) if locate_config_value(:ca_trust_file)
end
resolve_ssl_peer_fingerprint() click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 275
def resolve_ssl_peer_fingerprint
  locate_config_value(:ssl_peer_fingerprint)
end
resolve_target_nodes() click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 66
def resolve_target_nodes
  @list = case config[:manual]
         when true
           @name_args[0].split(" ")
         when false
           r = Array.new
           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/winops_winrm_knife_base.rb, line 248
def resolve_winrm_basic_auth
  locate_config_value(:winrm_authentication_protocol) == "basic"
end
resolve_winrm_disable_sspi() click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 279
def resolve_winrm_disable_sspi
  resolve_winrm_transport != :negotiate
end
resolve_winrm_kerberos_options() click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 252
def resolve_winrm_kerberos_options
  kerberos_opts = {}
  kerberos_opts[:keytab] = locate_config_value(:kerberos_keytab_file) if locate_config_value(:kerberos_keytab_file)
  kerberos_opts[:realm] = locate_config_value(:kerberos_realm) if locate_config_value(:kerberos_realm)
  kerberos_opts[:service] = locate_config_value(:kerberos_service) if locate_config_value(:kerberos_service)
  kerberos_opts
end
resolve_winrm_session_timeout() click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 243
def resolve_winrm_session_timeout
  #30 min (Default) OperationTimeout for long bootstraps fix for KNIFE_WINDOWS-8
  locate_config_value(:session_timeout).to_i * 60 if locate_config_value(:session_timeout)
end
resolve_winrm_transport() click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 260
def resolve_winrm_transport
  transport = locate_config_value(: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/winops_winrm_knife_base.rb, line 228
def resolve_winrm_user
  user = locate_config_value(: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/winops_winrm_knife_base.rb, line 113
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/winops_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)
    if ! config[:suppress_auth_failure]
      # Display errors if the caller hasn't opted to retry
      ui.error "Failed to authenticate to #{s.host} as #{locate_config_value(: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/winops_winrm_knife_base.rb, line 188
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/winops_winrm_knife_base.rb, line 182
def success_return_codes
  #Redundant if the CLI options parsing occurs
  return [0] unless config[:returns]
  return @success_return_codes ||= config[:returns].split(',').collect {|item| item.to_i}
end
validate_winrm_options!() click to toggle source
# File lib/chef/knife/winops_winrm_knife_base.rb, line 45
def validate_winrm_options!
  winrm_auth_protocol = locate_config_value(:winrm_authentication_protocol)

  if ! Chef::Knife::WinrmCore::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::WinrmCore::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/winops_winrm_knife_base.rb, line 291
          def warn_no_ssl_peer_verification
            if ! @@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