Class: SSHwrap::Main

Inherits:
Object
  • Object
show all
Defined in:
lib/sshwrap/main.rb

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Main

Returns a new instance of Main.



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
# File 'lib/sshwrap/main.rb', line 22

def initialize(options={})
  home = Dir.respond_to?(:home) ? Dir.home : ENV['HOME'] || ENV['LOGDIR']
  conffile = "#{home}/.sshwrap.yaml"
  if File.exist?(conffile)
    conf = YAML.load_file(conffile)
  else
    conf = {}
  end
  
  @mutex = Mutex.new
  @max_workers = options[:max_workers] || 1
  @abort_on_failure = options[:abort_on_failure]
  @user = options[:user] || Etc.getlogin
  @ssh_key = options[:ssh_key]
  @debug = options[:debug]
  @prompter = SSHwrap::Prompter.new
  @ssh_prompt = "Password for #{@user}: "
  
  if conf['password_regexp']
    @password_regexp = Regexp.new('(' + conf['password_regexp'] + ')')
  elsif options[:password_regexp]
    @password_regexp = options[:password_regexp]
  else
    # sudo on Mac OS X:
    #   Password:
    # sudo on Red Hat, Debian, Ubuntu:
    #   [sudo] password for <user>:
    @password_regexp = /(Password: |\[sudo\] password for .*: )/
  end
end

Instance Method Details

#ssh_execute(cmd, target) ⇒ Object



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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/sshwrap/main.rb', line 53

def ssh_execute(cmd, target)
  exitstatus = nil
  stdout = []
  stderr = []
  
  params = {}
  if @ssh_key
    params[:keys] = [@ssh_key]
  end
  using_password = false
  if @prompter.passwords[@ssh_prompt]
    using_password = true
    params[:password] = @prompter.passwords[@ssh_prompt]
  end
  
  begin
    Net::SSH.start(target, @user, params) do |ssh|
      puts "Connecting to #{target}" if @debug
      ch = ssh.open_channel do |channel|
        # Now we request a "pty" (i.e. interactive) session so we can send
        # data back and forth if needed.  It WILL NOT WORK without this,
        # and it has to be done before any call to exec.
        channel.request_pty do |ch_pty, success|
          if !success
            raise "Could not obtain pty (interactive ssh session) on #{target}"
          end
        end
        
        channel.exec(cmd) do |ch_exec, success|
          puts "Executing '#{cmd}' on #{target}" if @debug
          # 'success' isn't related to process exit codes or anything, but
          # more about ssh internals.  Not sure why it would fail at such
          # a basic level, but it seems smart to do something about it.
          if !success
            raise "SSH unable to execute command on #{target}"
          end
          
          # on_data is a hook that fires when ssh returns output data.  This
          # is what we've been doing all this for; now we can check to see
          # if it's a password prompt, and interactively return data if so
          # (see request_pty above).
          channel.on_data do |ch_data, data|
            if data =~ @password_regexp
              prompt = $1
              channel.send_data "#{@prompter.prompt(prompt)}\n"
            else
              stdout << data unless (data.nil? or data.empty?)
            end
          end
          
          channel.on_extended_data do |ch_onextdata, type, data|
            stderr << data unless (data.nil? or data.empty?)
          end
          
          channel.on_request "exit-status" do |ch_onreq, data|
            exitstatus = data.read_long
          end
        end
      end
      ch.wait
      ssh.loop
    end
  rescue Net::SSH::AuthenticationFailed
    if !using_password
      @prompter.prompt(@ssh_prompt)
      return ssh_execute(cmd, target)
    else
      stderr << "Authentication failed to #{target}"
    end
  rescue Exception => e
    stderr << "SSH connection error: #{e.message}"
  end
  
  [exitstatus, stdout, stderr]
end

#sshwrap(cmd, targets) ⇒ Object

cmd is a string or array of strings containing the command and arguments targets is an array of remote system hostnames



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/sshwrap/main.rb', line 131

def sshwrap(cmd, targets)
  cmdstring = nil
  if cmd.kind_of?(Array)
    cmdstring = cmd.join(' ')
  else
    cmdstring = cmd.to_s
  end
  
  statuses = {}
  
  threads = (1..@max_workers).map do |i|
    Thread.new("worker#{i}") do |tname|
      while true
        target = nil
        @mutex.synchronize do
          target = targets.shift
        end
        if !target
          break
        end
        puts "Thread #{tname} processing target #{target}" if @debug
        
        exitstatus, stdout, stderr = ssh_execute(cmdstring, target)
        statuses[target] = exitstatus
        
        @mutex.synchronize do
          puts '=================================================='
          if !stdout.empty?
            puts "Output from #{target}:"
            puts stdout.join
          end
          if !stderr.empty?
            puts "Error from #{target}:"
            puts stderr.join
          end
          puts "Exit status from #{target}: #{exitstatus}"
        end
        
        if @abort_on_failure && exitstatus != 0
          exit exitstatus
        end
      end
    end
  end
  
  threads.each(&:join)
  
  statuses
end