Module: KnifeSolo::SshCommand

Included in:
Chef::Knife::SoloBootstrap, Chef::Knife::SoloClean, Chef::Knife::SoloCook, Chef::Knife::SoloPrepare
Defined in:
lib/knife-solo/ssh_command.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(other) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/knife-solo/ssh_command.rb', line 11

def self.included(other)
  other.class_eval do
    # Lazy load our dependencies if the including class did not call
    # Knife#deps yet. Later calls to #deps override previous ones, so if
    # the outer class calls it, it should also call our #load_deps, i.e:
    #
    #   Include KnifeSolo::SshCommand
    #
    #   dep do
    #     require 'foo'
    #     require 'bar'
    #     KnifeSolo::SshCommand.load_deps
    #   end
    #
    deps { KnifeSolo::SshCommand.load_deps } unless defined?(@dependency_loader)

    option :ssh_config,
      :short       => '-F CONFIG_FILE',
      :long        => '--ssh-config-file CONFIG_FILE',
      :description => 'Alternate location for ssh config file'

    option :ssh_user,
      :short       => '-x USERNAME',
      :long        => '--ssh-user USERNAME',
      :description => 'The ssh username'

    option :ssh_password,
      :short       => '-P PASSWORD',
      :long        => '--ssh-password PASSWORD',
      :description => 'The ssh password'

    option :ssh_gateway,
      :long        => '--ssh-gateway GATEWAY',
      :description => 'The ssh gateway'

    option :ssh_control_master,
      :long => '--ssh-control-master SETTING',
      :description => 'Control master setting to use when running rsync (use "auto" to enable)',
      :default => 'no'

    option :identity_file,
      :long => "--identity-file IDENTITY_FILE",
      :description => "The SSH identity file used for authentication. [DEPRECATED] Use --ssh-identity-file instead."

    option :ssh_identity_file,
      :short => "-i IDENTITY_FILE",
      :long => "--ssh-identity-file IDENTITY_FILE",
      :description => "The SSH identity file used for authentication"

    option :forward_agent,
      :long        => '--forward-agent',
      :description => 'Forward SSH authentication. Adds -E to sudo, override with --sudo-command.',
      :boolean     => true,
      :default     => false

    option :ssh_port,
      :short       => '-p PORT',
      :long        => '--ssh-port PORT',
      :description => 'The ssh port'

    option :ssh_keepalive,
      :long        => '--[no-]ssh-keepalive',
      :description => 'Use ssh keepalive',
      :default     => true

    option :ssh_keepalive_interval,
      :long        => '--ssh-keepalive-interval SECONDS',
      :description => 'The ssh keepalive interval',
      :default     => 300,
      :proc        => Proc.new { |v| v.to_i }

    option :startup_script,
      :short       => '-s FILE',
      :long        => '--startup-script FILE',
      :description => 'The startup script on the remote server containing variable definitions'

    option :sudo_command,
      :long        => '--sudo-command SUDO_COMMAND',
      :description => 'The command to use instead of sudo for admin privileges'

    option :host_key_verify,
      :long => "--[no-]host-key-verify",
      :description => "Verify host key, enabled by default.",
      :boolean => true,
      :default => true

  end
end

.load_depsObject



4
5
6
7
8
9
# File 'lib/knife-solo/ssh_command.rb', line 4

def self.load_deps
  require 'knife-solo/ssh_connection'
  require 'knife-solo/tools'
  require 'net/ssh'
  require 'net/ssh/gateway'
end

Instance Method Details

#ask_passwordObject



141
142
143
144
145
146
# File 'lib/knife-solo/ssh_command.rb', line 141

def ask_password
  ui.ask("Enter the password for #{user}@#{host}: ") do |q|
    q.echo = false
    q.whitespace = :chomp
  end
end

#config_file_optionsObject



158
159
160
# File 'lib/knife-solo/ssh_command.rb', line 158

def config_file_options
  Net::SSH::Config.for(host, config_files)
end

#config_filesObject



187
188
189
# File 'lib/knife-solo/ssh_command.rb', line 187

def config_files
  Array(config[:ssh_config] || Net::SSH::Config.default_files)
end

#connection_optionsObject



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/knife-solo/ssh_command.rb', line 166

def connection_options
  options = config_file_options
  options[:port] = config[:ssh_port] if config[:ssh_port]
  options[:password] = config[:ssh_password] if config[:ssh_password]
  options[:keys] = [identity_file] if identity_file
  options[:gateway] = config[:ssh_gateway] if config[:ssh_gateway]
  options[:forward_agent] = true if config[:forward_agent]
  if !config[:host_key_verify]
    options[:paranoid] = false
    options[:user_known_hosts_file] = "/dev/null"
  end
  if config[:ssh_keepalive]
    options[:keepalive] = config[:ssh_keepalive]
    options[:keepalive_interval] = config[:ssh_keepalive_interval]
  end
  # Respect users' specification of config[:ssh_config]
  # Prevents Net::SSH itself from applying the default ssh_config files.
  options[:config] = false
  options
end

#custom_sudo_commandObject



226
227
228
229
230
231
# File 'lib/knife-solo/ssh_command.rb', line 226

def custom_sudo_command
  if sudo_command=config[:sudo_command]
    Chef::Log.debug("Using replacement sudo command: #{sudo_command}")
    return sudo_command
  end
end

#detect_authentication_methodObject



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/knife-solo/ssh_command.rb', line 191

def detect_authentication_method
  return @detected if @detected
  begin
    try_connection
  rescue Errno::ETIMEDOUT
    raise "Unable to connect to #{host}"
  rescue Net::SSH::AuthenticationFailed
    # Ensure the password is set or ask for it immediately
    password
  end
  @detected = true
end

#first_cli_arg_is_a_hostname?Boolean

Returns:

  • (Boolean)


100
101
102
# File 'lib/knife-solo/ssh_command.rb', line 100

def first_cli_arg_is_a_hostname?
  @name_args.first =~ /\A([^@]+(?>@)[^@]+|[^@]+?(?!@))\z/
end

#hostObject



137
138
139
# File 'lib/knife-solo/ssh_command.rb', line 137

def host
  host_descriptor[:host]
end

#host_descriptorObject



124
125
126
127
128
129
130
131
# File 'lib/knife-solo/ssh_command.rb', line 124

def host_descriptor
  return @host_descriptor if defined?(@host_descriptor)
  parts = @name_args.first.split('@')
  @host_descriptor = {
    :host => parts.pop,
    :user => parts.pop
  }
end

#identity_fileObject



162
163
164
# File 'lib/knife-solo/ssh_command.rb', line 162

def identity_file
  config[:ssh_identity] || config[:identity_file] || config[:ssh_identity_file]
end

#passwordObject



148
149
150
# File 'lib/knife-solo/ssh_command.rb', line 148

def password
  config[:ssh_password] ||= ask_password
end

#process_startup_file(command) ⇒ Object



272
273
274
# File 'lib/knife-solo/ssh_command.rb', line 272

def process_startup_file(command)
  command.insert(0, "source #{startup_script} && ")
end

#process_sudo(command) ⇒ Object



268
269
270
# File 'lib/knife-solo/ssh_command.rb', line 268

def process_sudo(command)
  command.gsub(/sudo/, sudo_command)
end

#processed_command(command, options = {}) ⇒ Object



280
281
282
283
284
# File 'lib/knife-solo/ssh_command.rb', line 280

def processed_command(command, options = {})
  command = process_sudo(command) if options[:process_sudo]
  command = process_startup_file(command) if startup_script
  command
end

#run_command(command, options = {}) ⇒ Object



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/knife-solo/ssh_command.rb', line 286

def run_command(command, options = {})
  defaults = {:process_sudo => true}
  options = defaults.merge(options)

  detect_authentication_method

  Chef::Log.debug("Initial command #{command}")

  command = processed_command(command, options)
  Chef::Log.debug("Running processed command #{command}")

  output = ui.stdout if options[:streaming]

  @connection ||= ssh_connection
  @connection.run_command(command, output)
end

#run_portable_mkdir_p(folder, mode = nil) ⇒ Object

TODO:

  • move this to a dedicated “portability” module?

  • use ruby in all cases instead?



321
322
323
324
325
326
327
328
329
# File 'lib/knife-solo/ssh_command.rb', line 321

def run_portable_mkdir_p(folder, mode = nil)
  if windows_node?
    # no mkdir -p on windows - fake it
    run_command %Q{ruby -e "require 'fileutils'; FileUtils.mkdir_p('#{folder}', :mode => #{mode})"}
  else
    mode_option = (mode.nil? ? "" : "-m #{mode}")
    run_command "mkdir -p #{mode_option} #{folder}"
  end
end

#run_with_fallbacks(commands, options = {}) ⇒ Object

Runs commands from the specified array until successful. Returns the result of the successful command or an ExecResult with exit_code 1 if all fail.



310
311
312
313
314
315
316
# File 'lib/knife-solo/ssh_command.rb', line 310

def run_with_fallbacks(commands, options = {})
  commands.each do |command|
    result = run_command(command, options)
    return result if result.success?
  end
  SshConnection::ExecResult.new(1)
end

#ssh_argsObject



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/knife-solo/ssh_command.rb', line 204

def ssh_args
  args = []

  args << [user, host].compact.join('@')

  args << "-F \"#{config[:ssh_config]}\"" if config[:ssh_config]
  args << "-i \"#{identity_file}\"" if identity_file
  args << "-o ForwardAgent=yes" if config[:forward_agent]
  args << "-p #{config[:ssh_port]}" if config[:ssh_port]
  args << "-o UserKnownHostsFile=\"#{connection_options[:user_known_hosts_file]}\"" if config[:host_key_verify] == false
  args << "-o StrictHostKeyChecking=no" if config[:host_key_verify] == false
  args << "-o ControlMaster=auto -o ControlPath=#{ssh_control_path} -o ControlPersist=3600" unless config[:ssh_control_master] == "no"

  args.join(' ')
end

#ssh_connectionObject



303
304
305
# File 'lib/knife-solo/ssh_command.rb', line 303

def ssh_connection
   SshConnection.new(host, user, connection_options, method(:password))
end

#ssh_control_pathObject



220
221
222
223
224
# File 'lib/knife-solo/ssh_command.rb', line 220

def ssh_control_path
  dir = File.join(ENV['HOME'], '.chef', 'knife-solo-sockets')
  FileUtils.mkdir_p(dir)
  File.join(dir, '%C')
end

#standard_sudo_commandObject



233
234
235
236
237
238
239
240
# File 'lib/knife-solo/ssh_command.rb', line 233

def standard_sudo_command
  return unless sudo_available?
  if config[:forward_agent]
    return 'sudo -E -p \'knife sudo password: \''
  else
    return 'sudo -p \'knife sudo password: \''
  end
end

#startup_scriptObject



246
247
248
# File 'lib/knife-solo/ssh_command.rb', line 246

def startup_script
  config[:startup_script]
end

#stream_command(command) ⇒ Object



276
277
278
# File 'lib/knife-solo/ssh_command.rb', line 276

def stream_command(command)
  run_command(command, :streaming => true)
end

#sudo_available?Boolean

Returns:

  • (Boolean)


261
262
263
264
265
266
# File 'lib/knife-solo/ssh_command.rb', line 261

def sudo_available?
  return @sudo_available unless @sudo_available.nil?
  @sudo_available = run_command('sudo -V', :process_sudo => false).success?
  Chef::Log.debug("`sudo` not available on #{host}") unless @sudo_available
  @sudo_available
end

#sudo_commandObject



242
243
244
# File 'lib/knife-solo/ssh_command.rb', line 242

def sudo_command
  custom_sudo_command || standard_sudo_command || ''
end

#try_connectionObject



152
153
154
155
156
# File 'lib/knife-solo/ssh_command.rb', line 152

def try_connection
  ssh_connection.session do |ssh|
    ssh.exec!("true")
  end
end

#userObject



133
134
135
# File 'lib/knife-solo/ssh_command.rb', line 133

def user
  host_descriptor[:user] || config_file_options[:user] || ENV['USER']
end

#validate_ssh_options!Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/knife-solo/ssh_command.rb', line 104

def validate_ssh_options!
  if config[:identity_file]
    ui.warn '`--identity-file` is deprecated, please use `--ssh-identity-file`.'
  end
  unless first_cli_arg_is_a_hostname?
    show_usage
    ui.fatal "You must specify [<user>@]<hostname> as the first argument"
    exit 1
  end
  if config[:ssh_user]
    host_descriptor[:user] ||= config[:ssh_user]
  end

  # NOTE: can't rely on default since it won't get called when invoked via knife bootstrap --solo
  if config[:ssh_keepalive_interval] && config[:ssh_keepalive_interval] <= 0
    ui.fatal '`--ssh-keepalive-interval` must be a positive number'
    exit 1
  end
end

#windows_node?Boolean

Returns:

  • (Boolean)


250
251
252
253
254
255
256
257
258
259
# File 'lib/knife-solo/ssh_command.rb', line 250

def windows_node?
  return @windows_node unless @windows_node.nil?
  @windows_node = run_command('ver', :process_sudo => false).stdout =~ /Windows/i
  if @windows_node
    Chef::Log.debug("Windows node detected")
  else
    @windows_node = false
  end
  @windows_node
end