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
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