Class: Chef::Knife::Ssh

Inherits:
Chef::Knife show all
Defined in:
lib/chef/knife/ssh.rb

Instance Attribute Summary collapse

Attributes inherited from Chef::Knife

#name_args, #ui

Instance Method Summary collapse

Methods inherited from Chef::Knife

#api_key, category, common_name, #configure_chef, #create_object, #delete_object, deps, #format_rest_error, guess_category, #highlight_config_error, #humanize_exception, #humanize_http_exception, inherited, #initialize, list_commands, load_commands, load_deps, msg, #noauth_rest, #parse_options, #read_config_file, reset_subcommands!, #rest, run, #run_with_pretty_exceptions, #server_url, #show_usage, snake_case_name, subcommand_category, subcommand_class_from, subcommand_loader, subcommands, subcommands_by_category, ui, unnamed?, #username

Methods included from Mixin::ConvertToClassName

#convert_to_class_name, #convert_to_snake_case, #filename_to_qualified_string, #snake_case_basename

Constructor Details

This class inherits a constructor from Chef::Knife

Instance Attribute Details

#password=(value) ⇒ Object (writeonly)

Sets the attribute password

Parameters:

  • value

    the value to set the attribute password to.


33
34
35
# File 'lib/chef/knife/ssh.rb', line 33

def password=(value)
  @password = value
end

Instance Method Details

#configure_attributeObject


311
312
313
314
315
# File 'lib/chef/knife/ssh.rb', line 311

def configure_attribute
  config[:attribute] = (config[:attribute] ||
                        Chef::Config[:knife][:ssh_attribute] ||
                        "fqdn").strip
end

#configure_identity_fileObject


331
332
333
334
# File 'lib/chef/knife/ssh.rb', line 331

def configure_identity_file
  config[:identity_file] = (config[:identity_file] || Chef::Config[:knife][:ssh_identity_file])
  config[:identity_file].strip! unless config[:identity_file].nil?
end

#configure_sessionObject


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/chef/knife/ssh.rb', line 108

def configure_session
  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 = format_for_display(item)[config[:attribute]]
             r.push(i) unless i.nil?
           end
           r
         end
  (ui.fatal("No nodes returned from search!"); exit 10) if list.length == 0
  session_from_list(list)
end

#configure_userObject


325
326
327
328
329
# File 'lib/chef/knife/ssh.rb', line 325

def configure_user
  config[:ssh_user] = (config[:ssh_user] ||
                       Chef::Config[:knife][:ssh_user])
  config[:ssh_user].strip! unless config[:ssh_user].nil?
end

#csshxObject


317
318
319
320
321
322
323
# File 'lib/chef/knife/ssh.rb', line 317

def csshx
  csshx_cmd = "csshX"
  session.servers_for.each do |server|
    csshx_cmd << " #{server.user ? "#{server.user}@#{server.host}" : server.host}"
  end
  exec(csshx_cmd)
end

#fixup_sudo(command) ⇒ Object


150
151
152
# File 'lib/chef/knife/ssh.rb', line 150

def fixup_sudo(command)
  command.sub(/^sudo/, 'sudo -p \'knife sudo password: \'')
end

#get_passwordObject


182
183
184
# File 'lib/chef/knife/ssh.rb', line 182

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

#interactiveObject


211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/chef/knife/ssh.rb', line 211

def interactive
  puts "Connected to #{ui.list(session.servers_for.collect { |s| ui.color(s.host, :cyan) }, :inline, " and ")}"
  puts
  puts "To run a command on a list of servers, do:"
  puts "  on SERVER1 SERVER2 SERVER3; COMMAND"
  puts "  Example: on latte foamy; echo foobar"
  puts
  puts "To exit interactive mode, use 'quit!'"
  puts
  while 1
    command = read_line
    case command
    when 'quit!'
      puts 'Bye!'
      break
    when /^on (.+?); (.+)$/
      raw_list = $1.split(" ")
      server_list = Array.new
      session.servers.each do |session_server|
        server_list << session_server if raw_list.include?(session_server.host)
      end
      command = $2
      ssh_command(command, session.on(*server_list))
    else
      ssh_command(command)
    end
  end
end

#mactermObject


287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/chef/knife/ssh.rb', line 287

def macterm
  begin
    require 'appscript'
  rescue LoadError
    STDERR.puts "you need the rb-appscript gem to use knife ssh macterm. `(sudo) gem install rb-appscript` to install"
    raise
  end

  Appscript.app("/Applications/Utilities/Terminal.app").windows.first.activate
  Appscript.app("System Events").application_processes["Terminal.app"].keystroke("n", :using=>:command_down)
  term = Appscript.app('Terminal')
  window = term.windows.first.get

  (session.servers_for.size - 1).times do |i|
    window.activate
    Appscript.app("System Events").application_processes["Terminal.app"].keystroke("t", :using=>:command_down)
  end

  session.servers_for.each_with_index do |server, tab_number|
    cmd = "unset PROMPT_COMMAND; echo -e \"\\033]0;#{server.host}\\007\"; ssh #{server.user ? "#{server.user}@#{server.host}" : server.host}"
    Appscript.app('Terminal').do_script(cmd, :in => window.tabs[tab_number + 1].get)
  end
end

154
155
156
157
158
159
160
161
162
# File 'lib/chef/knife/ssh.rb', line 154

def print_data(host, data)
  if data =~ /\n/
    data.split(/\n/).each { |d| print_data(host, d) }
  else
    padding = @longest - host.length
    str = ui.color(host, :cyan) + (" " * (padding + 1)) + data
    ui.msg(str)
  end
end

#read_lineObject

Present the prompt and read a single line from the console. It also detects ^D and returns “exit” in that case. Adds the input to the history, unless the input is empty. Loops repeatedly until a non-empty line is input.


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/chef/knife/ssh.rb', line 190

def read_line
  loop do
    command = reader.readline("#{ui.color('knife-ssh>', :bold)} ", true)

    if command.nil?
      command = "exit"
      puts(command)
    else
      command.strip!
    end

    unless command.empty?
      return command
    end
  end
end

#readerObject


207
208
209
# File 'lib/chef/knife/ssh.rb', line 207

def reader
  Readline
end

#runObject


336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/chef/knife/ssh.rb', line 336

def run
  extend Chef::Mixin::Command

  @longest = 0

  configure_attribute
  configure_user
  configure_identity_file
  configure_session

  case @name_args[1]
  when "interactive"
    interactive
  when "screen"
    screen
  when "tmux"
    tmux
  when "macterm"
    macterm
  when "csshx"
    csshx
  else
    ssh_command(@name_args[1..-1].join(" "))
  end

  session.close
end

#screenObject


240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/chef/knife/ssh.rb', line 240

def screen
  tf = Tempfile.new("knife-ssh-screen")
  if File.exist? "#{ENV["HOME"]}/.screenrc"
    tf.puts("source #{ENV["HOME"]}/.screenrc")
  end
  tf.puts("caption always '%-Lw%{= BW}%50>%n%f* %t%{-}%+Lw%<'")
  tf.puts("hardstatus alwayslastline 'knife ssh #{@name_args[0]}'")
  window = 0
  session.servers_for.each do |server|
    tf.print("screen -t \"#{server.host}\" #{window} ssh ")
    tf.print("-i #{config[:identity_file]} ") if config[:identity_file]
    server.user ? tf.puts("#{server.user}@#{server.host}") : tf.puts(server.host)
    window += 1
  end
  tf.close
  exec("screen -c #{tf.path}")
end

#sessionObject


85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/chef/knife/ssh.rb', line 85

def session
  config[:on_error] ||= :skip
  ssh_error_handler = Proc.new do |server|
    if config[:manual]
      node_name = server.host
    else
      @action_nodes.each do |n|
        node_name = n if format_for_display(n)[config[:attribute]] == server.host
      end
    end
    case config[:on_error]
    when :skip
      ui.warn "Failed to connect to #{node_name} -- #{$!.class.name}: #{$!.message}"
      $!.backtrace.each { |l| Chef::Log.debug(l) }
    when :raise
      #Net::SSH::Multi magic to force exception to be re-raised.
      throw :go, :raise
    end
  end

  @session ||= Net::SSH::Multi.start(:concurrent_connections => config[:concurrency], :on_error => ssh_error_handler)
end

#session_from_list(list) ⇒ Object


126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/chef/knife/ssh.rb', line 126

def session_from_list(list)
  list.each do |item|
    Chef::Log.debug("Adding #{item}")

    hostspec = config[:ssh_user] ? "#{config[:ssh_user]}@#{item}" : item
    session_opts = {}
    session_opts[:keys] = File.expand_path(config[:identity_file]) if config[:identity_file]
    session_opts[:password] = config[:ssh_password] if config[:ssh_password]
    session_opts[:port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
    session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug

    if config[:no_host_key_verify]
      session_opts[:paranoid] = false
      session_opts[:user_known_hosts_file] = "/dev/null"
    end

    session.use(hostspec, session_opts)

    @longest = item.length if item.length > @longest
  end

  session
end

#ssh_command(command, subsession = nil) ⇒ Object


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/chef/knife/ssh.rb', line 164

def ssh_command(command, subsession=nil)
  subsession ||= session
  command = fixup_sudo(command)
  subsession.open_channel do |ch|
    ch.request_pty
    ch.exec command do |ch, success|
      raise ArgumentError, "Cannot execute #{command}" unless success
      ch.on_data do |ichannel, data|
        print_data(ichannel[:host], data)
        if data =~ /^knife sudo password: /
          ichannel.send_data("#{get_password}\n")
        end
      end
    end
  end
  session.loop
end

#tmuxObject


258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/chef/knife/ssh.rb', line 258

def tmux
  ssh_dest = lambda do |server|
    identity = "-i #{config[:identity_file]} " if config[:identity_file]
    prefix = server.user ? "#{server.user}@" : ""
    "'ssh #{identity}#{prefix}#{server.host}'"
  end

  new_window_cmds = lambda do
    if session.servers_for.size > 1
      [""] + session.servers_for[1..-1].map do |server|
        "new-window -a -n '#{server.host}' #{ssh_dest.call(server)}"
      end
    else
      []
    end.join(" \\; ")
  end

  tmux_name = "'knife ssh #{@name_args[0].gsub(/:/,'=')}'"
  begin
    server = session.servers_for.first
    cmd = ["tmux new-session -d -s #{tmux_name}",
           "-n '#{server.host}'", ssh_dest.call(server),
           new_window_cmds.call].join(" ")
    Chef::Mixin::Command.run_command(:command => cmd)
    exec("tmux attach-session -t #{tmux_name}")
  rescue Chef::Exceptions::Exec
  end
end