scutil 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.rdoc CHANGED
@@ -1,6 +1,11 @@
1
1
 
2
2
  ==Changelog
3
3
 
4
+ ===0.4.0 | 2011-02-21
5
+
6
+ * Added built-in sudo password support.
7
+ * Some new tests and documentation fixes.
8
+
4
9
  ===0.3.3 | 2011-12-20
5
10
 
6
11
  * Fixed bug when writing file to disk and then immediately reading the
data/README.rdoc CHANGED
@@ -3,42 +3,39 @@
3
3
 
4
4
  ==Description:
5
5
 
6
- Scutil <em>(pronounced "scuttle")</em> is a small Ruby library that
7
- makes using {Net::SSH}[http://net-ssh.github.com] to execute commands
8
- on remote systems even more convenient.
6
+ Scutil <em>(pronounced "scuttle")</em> is a small Ruby library that makes using
7
+ {Net::SSH}[http://net-ssh.github.com] to execute commands on remote systems even
8
+ more convenient.
9
9
 
10
10
  It does this in three ways:
11
11
 
12
- First, it defines Scutil.exec_command which abstracts the callbacks
13
- and other setup necessary to connect to a system, execute a command,
14
- and capture the output and return value of that command. You can
15
- roughly think of it as a generic <em>"Capistrano lite"</em> in this
16
- regard.
12
+ First, it defines Scutil.exec_command which abstracts the callbacks and other
13
+ setup necessary to connect to a system, execute a command, and capture the
14
+ output and return value of that command. You can roughly think of it as a
15
+ generic <em>"Capistrano lite"</em> in this regard.
17
16
 
18
- Second, it tracks the connections used on all systems and reuses
19
- these connections where ever possible.
17
+ Second, it tracks the connections used on all systems and reuses these
18
+ connections where ever possible.
20
19
 
21
- Finally, scutil takes away the burden of managing data transfers over
22
- PTY connections. It automatically requests PTYs where needed and
23
- makes them "binary safe" (this functionality is configurable and can
24
- be disabled). PTYs are needed for some curses-based programs and,
25
- most importantly in scutil's context, for sudo. A full discussion on
26
- PTYs is beyond the scope of this documentation.
20
+ Finally, scutil takes away the burden of managing data transfers over PTY
21
+ connections. It automatically requests PTYs where needed and makes them "binary
22
+ safe" (this functionality is configurable and can be disabled). PTYs are needed
23
+ for some curses-based programs and, most importantly in scutil's context, for
24
+ sudo. A full discussion on PTYs is beyond the scope of this documentation.
27
25
 
28
26
  The "_automatic_" part of PTY requests comes from a regex in
29
- Scutil.exec_command. Basically, if _sudo_ is at the start of the
30
- command to be executed, scutil will request a PTY. This regex is
31
- configurable through +:scutil_pty_regex+. You can force a PTY request
32
- by specifying +:scutil_force_pty+ in the various _options_ arguments.
27
+ Scutil.exec_command. Basically, if _sudo_ is at the start of the command to be
28
+ executed, scutil will request a PTY. This regex is configurable through
29
+ +:scutil_pty_regex+. You can force a PTY request by specifying
30
+ +:scutil_force_pty+ in the various _options_ arguments.
33
31
 
34
32
  All of this syntactic sugar can be used as a simple class method with
35
- Scutil.exec_command, as an instantiable class with Scutil::Exec, or as
36
- a mixin with the module Scutil.
33
+ Scutil.exec_command, as an instantiable class with Scutil::Exec, or as a mixin
34
+ with the module Scutil.
37
35
 
38
36
  ==Synopsis:
39
37
 
40
- You can use scutil in a few different ways, for more usage examples
41
- see Scutil.
38
+ You can use scutil in a few different ways, for more usage examples see Scutil.
42
39
 
43
40
  require 'scutil'
44
41
 
@@ -72,7 +69,7 @@ see Scutil.
72
69
  require 'stringio'
73
70
 
74
71
  command_output = StringIO.new
75
- Scutil.exec_command('servername1', 'mas', 'sudo cat /root/secrets.txt', command_output)
72
+ Scutil.exec_command('servername1', 'sudo cat /root/secrets.txt', command_output)
76
73
  puts command_output.string
77
74
 
78
75
  == Installation:
@@ -83,22 +80,21 @@ see Scutil.
83
80
 
84
81
  The MIT License (MIT)
85
82
 
86
- Copyright (c) 2011 by Marc Soda
83
+ Copyright (c) 2012 by Marc Soda
87
84
 
88
- Permission is hereby granted, free of charge, to any person obtaining a copy
89
- of this software and associated documentation files (the "Software"), to deal
90
- in the Software without restriction, including without limitation the rights
91
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
92
- copies of the Software, and to permit persons to whom the Software is
93
- furnished to do so, subject to the following conditions:
85
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
86
+ this software and associated documentation files (the "Software"), to deal in
87
+ the Software without restriction, including without limitation the rights to
88
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
89
+ the Software, and to permit persons to whom the Software is furnished to do so,
90
+ subject to the following conditions:
94
91
 
95
- The above copyright notice and this permission notice shall be included in
96
- all copies or substantial portions of the Software.
92
+ The above copyright notice and this permission notice shall be included in all
93
+ copies or substantial portions of the Software.
97
94
 
98
95
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
99
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
100
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
101
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
102
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
103
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
104
- THE SOFTWARE.
96
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
97
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
98
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
99
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
100
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/lib/scutil/exec.rb CHANGED
@@ -15,7 +15,7 @@ module Scutil
15
15
  class Exec
16
16
  include Scutil
17
17
  attr_reader :hostname,:username
18
-
18
+
19
19
  # Defaults to current user (ENV['USER'] if _username_ is not
20
20
  # specified.
21
21
  def initialize(hostname, username=nil, options={})
@@ -35,9 +35,8 @@ module Scutil
35
35
  set_options(options)
36
36
  Scutil.exec_command(@hostname, @username, cmd, output, @options)
37
37
  end
38
-
38
+
39
39
  def set_options(options={})
40
- # Local map has precedence.
41
40
  @options.merge!(options)
42
41
  end
43
42
 
@@ -49,7 +48,7 @@ module Scutil
49
48
  sys_conn = Scutil.connection_cache.fetch(@hostname)
50
49
  conn = sys_conn.get_connection(@hostname, @username, pty_needed, @options)
51
50
  else
52
- sys_conn = SystemConnection.new(@hostname)
51
+ sys_conn = SystemConnection.new(@hostname, @options)
53
52
  conn = sys_conn.get_connection(@hostname, @username, pty_needed, @options)
54
53
  end
55
54
  conn
@@ -19,9 +19,9 @@ module Scutil
19
19
  conn = nil
20
20
  # Local map has precedence.
21
21
  @options.merge!(options)
22
-
23
- scrub_options @options
24
-
22
+
23
+ ssh_options = scrub_options @options
24
+ options = @options
25
25
  if (pty_needed)
26
26
  if !@pty_connection.nil?
27
27
  # Existing PTY connection
@@ -31,7 +31,7 @@ module Scutil
31
31
 
32
32
  # New PTY connection
33
33
  print "[#{hostname}] Opening new channel (pty) to system...\n" if @options[:scutil_verbose]
34
- conn = Net::SSH.start(hostname, username, @options)
34
+ conn = Net::SSH.start(hostname, username, ssh_options)
35
35
  @pty_connection = conn
36
36
  else
37
37
  if !@connection.nil?
@@ -42,18 +42,19 @@ module Scutil
42
42
 
43
43
  # New non-PTY connection
44
44
  print "[#{hostname}] Opening channel (non-pty) to system...\n" if @options[:scutil_verbose]
45
- conn = Net::SSH.start(hostname, username, @options)
45
+ conn = Net::SSH.start(hostname, username, ssh_options)
46
46
  @connection = conn
47
47
  end
48
-
49
48
  return conn
50
49
  end
51
50
 
52
51
  # Remove scutil specific options. The rest go to Net::SSH.
53
52
  def scrub_options(options)
54
- options.delete(:scutil_verbose) if (options.has_key?(:scutil_verbose))
55
- options.delete(:scutil_force_pty) if (options.has_key?(:scutil_force_pty))
56
- options.delete(:scutil_pty_regex) if (options.has_key?(:scutil_pty_regex))
53
+ ssh_options = {}
54
+ options.each do |k, v|
55
+ ssh_options[k] = v if (k !~ /^scutil_+/)
56
+ end
57
+ return ssh_options
57
58
  end
58
59
 
59
60
  def to_s
data/lib/scutil.rb CHANGED
@@ -2,25 +2,24 @@
2
2
  =begin
3
3
  The MIT License (MIT)
4
4
 
5
- Copyright (C) 2011 by Marc Soda
5
+ Copyright (C) 2012 by Marc Soda
6
6
 
7
- Permission is hereby granted, free of charge, to any person obtaining a copy
8
- of this software and associated documentation files (the "Software"), to deal
9
- in the Software without restriction, including without limitation the rights
10
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- copies of the Software, and to permit persons to whom the Software is
12
- furnished to do so, subject to the following conditions:
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
8
+ this software and associated documentation files (the "Software"), to deal in
9
+ the Software without restriction, including without limitation the rights to
10
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11
+ the Software, and to permit persons to whom the Software is furnished to do so,
12
+ subject to the following conditions:
13
13
 
14
- The above copyright notice and this permission notice shall be included in
15
- all copies or substantial portions of the Software.
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
16
 
17
17
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
- THE SOFTWARE.
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
23
  =end
25
24
 
26
25
  require 'net/ssh'
@@ -30,96 +29,105 @@ require 'scutil/connection_cache'
30
29
  require 'scutil/system_connection'
31
30
 
32
31
  module Scutil
33
- SCUTIL_VERSION = '0.3.3'
32
+ SCUTIL_VERSION = '0.4.0'
33
+
34
34
  # By default, buffer 10M of data before writing.
35
35
  DEFAULT_OUTPUT_BUFFER_SIZE = 0xA00000
36
+
36
37
  # Checks for a command starting with _sudo_ by default.
37
38
  DEFAULT_PTY_REGEX = /^\s*sudo/
39
+
40
+ # Default password prompt is _[sudo] password for_. Redhat based systems use
41
+ # _Password:_ instead.
42
+ DEFAULT_SUDO_PASSWD_REGEX = /^\[sudo\] password for/
43
+ # DEFAULT_PASSWD_REGEX = /^Password:/
44
+
45
+ # Default password failed prompt is _Sorry, try again_.
46
+ DEFAULT_SUDO_PASSWD_FAILED_REGEX = /^Sorry, try again/
47
+
38
48
  @connection_cache = ConnectionCache.new
39
49
  @output_buffer_size = DEFAULT_OUTPUT_BUFFER_SIZE
40
50
 
41
51
  class << self
42
- # All successfully established connections end up here for reuse
43
- # later.
52
+ # All successfully established connections end up here for reuse later.
44
53
  attr_accessor :connection_cache
45
- # Set to 10M by default, this can be adjusted to tell scutil when
46
- # to write command output to _output_.
54
+ # Set to 10M by default, this can be adjusted to tell scutil when to write
55
+ # command output to _output_.
47
56
  attr_accessor :output_buffer_size
48
57
 
49
58
  # Should we request a PTY? Uses custom regex if defined in
50
59
  # +:scutil_pty_regex+.
51
60
  #
52
- def check_pty_needed?(cmd, options, hostname)
53
- regex = DEFAULT_PTY_REGEX
54
- if (options[:scutil_force_pty].nil?)
55
- # If a custom regex has been defined, use it.
56
- if (!options[:scutil_pty_regex].nil?)
57
- if options[:scutil_pty_regex].kind_of? Regexp
58
- regex = options[:scutil_pty_regex]
59
- else
60
- raise Scutil::Error.new("Error: :scutil_pty_regex must be a kind of Regexp", hostname)
61
- end
62
- else
63
- return (cmd =~ regex) ? true : false
64
- end
65
- else
66
- return options[:scutil_force_pty] ? true : false
61
+ def check_pty_needed?(cmd, options, hostname)
62
+ if options[:scutil_force_pty]
63
+ return true
64
+ end
65
+
66
+ if !options[:scutil_pty_regex].kind_of? Regexp
67
+ raise Scutil::Error.new(":scutil_pty_regex must be a kind of Regexp", hostname)
67
68
  end
69
+ return (cmd =~ options[:scutil_pty_regex]) ? true : false
68
70
  end
69
71
 
70
72
  # Drops all instances of +hostname+ from @connection_cache.
71
73
  def clear!(hostname)
72
74
  if (Scutil.connection_cache.exists?(hostname))
73
75
  Scutil.connection_cache.remove(hostname)
74
- else
75
- raise Scutil::Error.new("Error: :scutil_pty_regex must be a kind of Regexp", hostname)
76
76
  end
77
77
  end
78
78
 
79
- # Scutil.exec_command is used to execute a command, specified in
80
- # _cmd_, on a remote system. The return value and any ouput of
81
- # the command are captured.
79
+ # Scutil.exec_command is used to execute a command, specified in _cmd_, on a
80
+ # remote system. The return value and any ouput of the command are
81
+ # captured.
82
82
  #
83
- # If _output_ is a string it will be treated as a filename to be
84
- # opened (mode 'w') and all command output will be written to
85
- # this file. If _output_ is an IO object it will be treated as an
86
- # open file handle.* Finally, if _output_ is omitted, or an empty
87
- # string, all command output will be directed to _$stdout_.
83
+ # If _output_ is a string it will be treated as a filename to be opened
84
+ # (mode 'w') and all command output will be written to this file. If
85
+ # _output_ is an IO object it will be treated as an open file handle.*
86
+ # Finally, if _output_ is omitted, or an empty string, all command output
87
+ # will be directed to _$stdout_.
88
88
  #
89
- # <em>*NB:* This isn't actually true. The only check made is to
90
- # see if _output_ responds to +:write+. The idea being that not
91
- # only will a file handle have a +write+ method, but also
92
- # something like +StringIO+. Using +StringIO+ here makes it easy
93
- # to capture the command's output in a string. Suggestions on a
94
- # better way to do this are definitely welcome.</em>
89
+ # <em>*NB:* This isn't actually true. The only check made is to see if
90
+ # _output_ responds to +:write+. The idea being that not only will a file
91
+ # handle have a +write+ method, but also something like +StringIO+. Using
92
+ # +StringIO+ here makes it easy to capture the command's output in a string.
93
+ # Suggestions on a better way to do this are definitely welcome.</em>
95
94
  #
96
- # Scutil will automatically request a PTY if _sudo_ is at the
97
- # start of _cmd_. This is driven by a regex which is customizable
98
- # via the option +:scutil_pty_regex+. You can also force a PTY
99
- # request by specifying +:scutil_force_pty+ in _options_.
95
+ # Scutil will automatically request a PTY if _sudo_ is at the start of
96
+ # _cmd_. This is driven by a regex which is customizable via the option
97
+ # +:scutil_pty_regex+. You can also force a PTY request by specifying
98
+ # +:scutil_force_pty+ in _options_.
100
99
  #
101
100
  # Scutil.exec_command takes the following options:
102
101
  #
103
- # * :scutil_verbose => Extra output.
104
- # * :scutil_force_pty => Force a PTY request (or not) for every channel.
105
- # * :scutil_pty_regex => Specific a custom regex here for use when scutil decides whether or not to request a PTY.
102
+ # * :scutil_verbose => Extra output.
103
+ # * :scutil_force_pty => Force a PTY request (or not) for every channel.
104
+ # * :scutil_pty_regex => Specific a custom regex here for use when scutil
105
+ # decides whether or not to request a PTY.
106
+ # * :scutil_sudo_passwd_regex => If sudo requires a password you can specify the
107
+ # prompt to look for, e.g., _Password:_ .
108
+ # * :scutil_sudo_passwd_failed_regex => Regular expression for a sudo password failure.
109
+ # * :scutil_sudo_passwd => The sudo password.
106
110
  #
107
- # In addition, any other options passed Scutil.exec_command will
108
- # be passed on to Net::SSH, _except_ those prefixed with
109
- # _scutil__.
111
+ # In addition, any other options passed Scutil.exec_command will be passed
112
+ # on to Net::SSH, _except_ those prefixed with _scutil__.
110
113
  #
111
- # All calls to Scutil.exec_command, regardless of the way it's
112
- # used, will return the remote command's return value.
114
+ # All calls to Scutil.exec_command, regardless of the way it's used, will
115
+ # return the remote command's return value.
113
116
  #
114
117
  # retval = Scutil.exec_command('hostname', 'username', '/bin/true')
115
118
  # puts "True is false!" if retval != 0
116
119
  #
117
- def exec_command(hostname, username, cmd, output=nil, options={})
120
+ # See the test/ directory for more usage examples.
121
+ def exec_command(hostname, username, cmd, output=nil, new_options={})
122
+ # Fill in defaults
123
+ options = get_default_options
124
+ options.merge! new_options
125
+
118
126
  # Do we need a PTY?
119
127
  pty_needed = check_pty_needed? cmd, options, hostname
120
128
 
121
- # Check for an existing connection in the cache based on the hostname. If the
122
- # hostname exists find a suitable connection.
129
+ # Check for an existing connection in the cache based on the hostname. If
130
+ # the hostname exists find a suitable connection.
123
131
  conn = nil
124
132
  begin
125
133
  if (Scutil.connection_cache.exists?(hostname))
@@ -127,47 +135,99 @@ module Scutil
127
135
  print "[#{hostname}] Using existing connection\n" if options[:scutil_verbose]
128
136
  conn = sys_conn.get_connection(hostname, username, pty_needed, options)
129
137
  else
130
- sys_conn = SystemConnection.new(hostname)
138
+ sys_conn = SystemConnection.new(hostname, options)
131
139
  # Call get_connection first. Don't add to cache unless established.
132
140
  conn = sys_conn.get_connection(hostname, username, pty_needed, options)
133
141
  print "[#{hostname}] Adding new connection to cache\n" if options[:scutil_verbose]
134
142
  Scutil.connection_cache << sys_conn
135
143
  end
136
144
  rescue Net::SSH::AuthenticationFailed => err
137
- raise Scutil::Error.new("Error: Authenication failed for user: #{username}", hostname)
145
+ raise Scutil::Error.new("Authenication failed for user: #{username}", hostname)
138
146
  rescue SocketError => err
139
- raise Scutil::Error.new("Error: " + err.message, hostname)
147
+ raise Scutil::Error.new(err.message, hostname)
140
148
  end
141
149
 
142
150
  fh = $stdout
143
151
  if (output.nil?)
144
152
  fh = $stdout
145
- elsif (output.respond_to? :write)
146
- # XXX: This may not be a safe assumuption...
153
+ elsif (output.respond_to? :write) # XXX: This may not be a safe assumuption...
147
154
  fh = output
148
155
  elsif (output.class == String)
149
156
  fh = File.open(output, 'w') unless output.empty?
150
157
  else
151
- raise Scutil::Error.new("Error: Invalid output object type: #{output.class}.", hostname)
158
+ raise Scutil::Error.new("Invalid output object type: #{output.class}.", hostname)
159
+ end
160
+
161
+ # If a custom password prompt regex has been defined, use it.
162
+ if (!options[:scutil_sudo_passwd_regex].nil? &&
163
+ (options[:scutil_sudo_passwd_regex].kind_of? Regexp))
164
+ passwd_regex = options[:scutil_sudo_passwd_regex]
165
+ else
166
+ raise Scutil::Error.new(":scutil_sudo_passwd_regex must be a kind of Regexp", hostname)
167
+ end
168
+
169
+ # If a custom bad password prompt regex has been defined, use it.
170
+ if (!options[:scutil_sudo_passwd_failed_regex].nil? &&
171
+ (options[:scutil_sudo_passwd_failed_regex].kind_of? Regexp))
172
+ passwd_failed_regex = options[:scutil_sudo_passwd_failed_regex]
173
+ else
174
+ raise Scutil::Error.new(":scutil_sudo_passwd_failed_regex must be a kind of Regexp", hostname)
152
175
  end
153
176
 
154
177
  # Setup channel callbacks
155
178
  odata = ""
156
179
  edata = ""
157
180
  exit_status = 0
181
+ # Catch the first call to on_data
182
+ sudo_passwd_state = :new
158
183
  chan = conn.open_channel do |channel|
159
184
  print "[#{conn.host}:#{channel.local_id}] Setting up callbacks...\n" if options[:scutil_verbose]
160
185
  if (pty_needed)
161
186
  print "[#{conn.host}:#{channel.local_id}] Requesting PTY...\n" if options[:scutil_verbose]
162
187
  # OPOST is necessary, CS8 makes sense. Revisit after broader testing.
163
- channel.request_pty(:modes => { Net::SSH::Connection::Term::CS8 => 1, Net::SSH::Connection::Term::OPOST => 0 } ) do |ch, success|
188
+ channel.request_pty(:modes => { Net::SSH::Connection::Term::CS8 => 1,
189
+ Net::SSH::Connection::Term::OPOST => 0 } ) do |ch, success|
164
190
  raise Scutil::Error.new("Failed to get a PTY", hostname) if !success
165
191
  end
166
192
  end
167
193
 
168
194
  channel.on_data do |ch, data|
169
195
  # print "on_data: #{data.size}\n" if options[:scutil_verbose]
170
- odata += data
196
+
197
+ # sudo password states are as follows:
198
+ # :new => Connection established, first data packet.
199
+ # :waiting => Password sent, wating for reply.
200
+ # :done => Authenication complete or not required.
201
+ case (sudo_passwd_state)
202
+ when :new
203
+ if (data =~ passwd_regex)
204
+ if (options[:scutil_sudo_passwd].nil?)
205
+ # No password defined
206
+ raise Scutil::Error.new("[#{conn.host}:#{channel.local_id}] Password required for sudo.
207
+ Define in :scutil_sudo_passwd.", hostname)
208
+ channel.close
209
+ end
210
+ ch.send_data options[:scutil_sudo_passwd] + "\n"
211
+ sudo_passwd_state = :waiting
212
+ else
213
+ odata += data
214
+ sudo_passwd_state = :done
215
+ end
216
+ when :waiting
217
+ # if (data == "\n")
218
+ if (data =~ passwd_failed_regex)
219
+ # Bad sudo password
220
+ raise Scutil::Error.new("[#{conn.host}:#{channel.local_id}] Password failed for sudo.
221
+ Define in :scutil_sudo_passwd or check :scutil_sudo_failed_passwd for the correct failure response.",
222
+ hostname)
223
+ channel.close
224
+ sudo_passwd_state = :done
225
+ else
226
+ # NoOp for "\n"
227
+ end
228
+ else
229
+ odata += data
230
+ end
171
231
 
172
232
  # Only buffer some of the output before writing to disk (10M by default).
173
233
  if (odata.size >= Scutil.output_buffer_size)
@@ -177,7 +237,7 @@ module Scutil
177
237
  end
178
238
 
179
239
  channel.on_extended_data do |ch, type, data|
180
- # print "on_extended_data: #{data.size}\n" if options[:scutil_verbose]
240
+ print "on_extended_data: #{data.size}\n" if options[:scutil_verbose]
181
241
  edata += data
182
242
  end
183
243
 
@@ -198,18 +258,31 @@ module Scutil
198
258
  end
199
259
 
200
260
  conn.loop
201
-
261
+
202
262
  # Write whatever is left
203
263
  fh.write odata
204
264
 
205
265
  # Close the file or you'll chase red herrings for two hours...
206
266
  fh.close unless fh == $stdout
207
-
267
+
208
268
  # If extended_data was recieved there was a problem...
209
269
  raise Scutil::Error.new("Error: #{edata}", hostname, exit_status) unless (edata.empty?)
210
-
270
+
211
271
  # The return value of the remote command.
212
272
  return exit_status
213
273
  end
274
+
275
+ private
276
+ # Set the default options for connection.
277
+ def get_default_options
278
+ {
279
+ :scutil_verbose => false,
280
+ :scutil_force_pty => false,
281
+ :scutil_pty_regex => DEFAULT_PTY_REGEX,
282
+ :scutil_sudo_passwd_regex => DEFAULT_PASSWD_REGEX,
283
+ :scutil_sudo_passwd_failed_regex => DEFAULT_PASSWD_FAILED_REGEX,
284
+ :scutil_sudo_passwd => nil
285
+ }
286
+ end
214
287
  end
215
288
  end
data/scutil.gemspec CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  Gem::Specification.new do |s|
3
3
  s.name = 'scutil'
4
- s.version = '0.3.3'
5
- s.date = '2011-12-20'
4
+ s.version = '0.4.0'
5
+ s.date = '2012-02-21'
6
6
  s.summary = 'SSH Command UTILity'
7
7
  s.description = <<-EOF
8
8
  Scutil is a library for conveniently executing commands
data/test/test_scutil.rb CHANGED
@@ -8,6 +8,8 @@ class TestScutil < Test::Unit::TestCase
8
8
  TRUE_COMMAND = '/bin/true'
9
9
  FALSE_COMMAND = '/bin/false'
10
10
  FAKE_COMMAND = '/bin/no_such_command'
11
+ # VERBOSE = true
12
+ VERBOSE = false
11
13
 
12
14
  @hostname = nil
13
15
  @port = nil
@@ -32,7 +34,8 @@ class TestScutil < Test::Unit::TestCase
32
34
  @tmp_output = nil
33
35
  @exec = Scutil::Exec.new(TestScutil.hostname, TestScutil.user,
34
36
  { :port => TestScutil.port,
35
- :scutil_verbose => false })
37
+ :scutil_verbose => VERBOSE
38
+ })
36
39
  end
37
40
 
38
41
  def teardown
@@ -72,7 +75,6 @@ class TestScutil < Test::Unit::TestCase
72
75
  def test_exec_command_output
73
76
  divert_stdout
74
77
  @exec.exec_command('echo "alpha"')
75
- # Scutil.exec_command(TestScutil.hostname, TestScutil.user, 'echo "alpha"', nil, { :port => TestScutil.port })
76
78
  revert_stdout
77
79
  assert_equal "alpha", @output.string.chomp
78
80
  end
@@ -98,13 +100,27 @@ class TestScutil < Test::Unit::TestCase
98
100
  end
99
101
 
100
102
  def test_pty_requested
101
- @exec.exec_command("sudo " + TRUE_COMMAND)
103
+ # XXX: check retvals everywhere!
104
+ ret_val = @exec.exec_command("sudo " + TRUE_COMMAND)
102
105
  conn = Scutil.connection_cache.fetch(TestScutil.hostname)
103
106
  assert_not_nil(conn.pty_connection)
104
107
  assert_instance_of(Net::SSH::Connection::Session, conn.pty_connection)
105
108
  assert_nil(conn.connection)
109
+ assert_equal(0, ret_val)
106
110
  end
107
-
111
+
112
+ def test_pty_requested_and_echo
113
+ divert_stdout
114
+ ret_val = @exec.exec_command('sudo ' + 'echo "bravo"')
115
+ revert_stdout
116
+ conn = Scutil.connection_cache.fetch(TestScutil.hostname)
117
+ assert_not_nil(conn.pty_connection)
118
+ assert_instance_of(Net::SSH::Connection::Session, conn.pty_connection)
119
+ assert_nil(conn.connection)
120
+ assert_equal(0, ret_val)
121
+ assert_equal "bravo", @output.string.chomp
122
+ end
123
+
108
124
  def test_option_pty_regex
109
125
  @exec.exec_command("env " + TRUE_COMMAND, nil, { :scutil_pty_regex => /^env / })
110
126
  conn = Scutil.connection_cache.fetch(TestScutil.hostname)
@@ -119,7 +135,7 @@ class TestScutil < Test::Unit::TestCase
119
135
  revert_stdout
120
136
  assert_match(/\[#{TestScutil.hostname}\]/, @output.string)
121
137
  end
122
-
138
+
123
139
  def test_option_force_pty
124
140
  @exec.exec_command(TRUE_COMMAND, nil, { :scutil_force_pty => true })
125
141
  conn = Scutil.connection_cache.fetch(TestScutil.hostname)
@@ -141,13 +157,54 @@ class TestScutil < Test::Unit::TestCase
141
157
  revert_stdout
142
158
  assert_match(/\[#{TestScutil.hostname}\]/, @output.string)
143
159
  end
160
+
161
+ # def test_sudo_passwd
162
+ # ret_val = @exec.exec_command('sudo ' + TRUE_COMMAND, nil, { :scutil_sudo_passwd => "p4ssw0rd", :scutil_sudo_passwd_regex => /^Password:/ })
163
+ # conn = Scutil.connection_cache.fetch(TestScutil.hostname)
164
+ # assert_not_nil(conn.pty_connection)
165
+ # assert_instance_of(Net::SSH::Connection::Session, conn.pty_connection)
166
+ # assert_nil(conn.connection)
167
+ # assert_equal(0, ret_val)
168
+ # end
169
+ #
170
+ # def test_bad_sudo_passwd
171
+ # assert_raises(Scutil::Error) do
172
+ # @exec.exec_command('sudo ' + TRUE_COMMAND, nil, { :scutil_sudo_passwd => "4ssw0rd", :scutil_sudo_passwd_regex => /^Password:/ })
173
+ # end
174
+ # conn = Scutil.connection_cache.fetch(TestScutil.hostname)
175
+ # assert_not_nil(conn.pty_connection)
176
+ # assert_instance_of(Net::SSH::Connection::Session, conn.pty_connection)
177
+ # assert_nil(conn.connection)
178
+ # end
179
+ #
180
+ # def test_no_sudo_passwd
181
+ # assert_raises(Scutil::Error) do
182
+ # @exec.exec_command('sudo ' + TRUE_COMMAND, nil, { :scutil_sudo_passwd_regex => /^Password:/ })
183
+ # end
184
+ # conn = Scutil.connection_cache.fetch(TestScutil.hostname)
185
+ # assert_not_nil(conn.pty_connection)
186
+ # assert_instance_of(Net::SSH::Connection::Session, conn.pty_connection)
187
+ # assert_nil(conn.connection)
188
+ # end
189
+ #
190
+ # def test_sudo_passwd_output
191
+ # divert_stdout
192
+ # ret_val = @exec.exec_command('sudo ' + 'echo "charlie"', nil, { :scutil_sudo_passwd => "p4ssw0rd" })
193
+ # revert_stdout
194
+ # conn = Scutil.connection_cache.fetch(TestScutil.hostname)
195
+ # assert_not_nil(conn.pty_connection)
196
+ # assert_instance_of(Net::SSH::Connection::Session, conn.pty_connection)
197
+ # assert_nil(conn.connection)
198
+ # assert_equal(0, ret_val)
199
+ # assert_equal "charlie", @output.string.chomp
200
+ # end
144
201
  end
145
202
 
146
203
  class TestScutilAlt < Test::Unit::TestCase
147
204
  TRUE_COMMAND = '/bin/true'
148
205
  FALSE_COMMAND = '/bin/false'
149
206
  FAKE_COMMAND = '/bin/no_such_command'
150
-
207
+
151
208
  @hostname = nil
152
209
  @port = nil
153
210
  @user = nil
@@ -198,18 +255,17 @@ class TestScutilAlt < Test::Unit::TestCase
198
255
  revert_stdout
199
256
  assert_equal "alpha", @output.string.chomp
200
257
  end
201
-
258
+
202
259
  def teardown
203
260
  Scutil.connection_cache.remove_all
204
261
  end
205
262
  end
206
263
 
207
- # XXX: This breaks --name, et al.
208
- if ARGV[0].nil?
209
- puts "Usage: #{$0} host[:port]"
264
+ if (ARGV[0].nil? || (ARGV[0] !~ /\w+:\d+/))
265
+ puts "Usage: #{$0} host:port"
210
266
  exit(1)
211
267
  end
212
268
 
213
- (TestScutil.hostname, TestScutil.port) = ARGV[0].split(':')
214
- ARGV[0] = nil;
269
+ connect_string = ARGV.shift
270
+ (TestScutil.hostname, TestScutil.port) = connect_string.split(':')
215
271
  TestScutil.user = 'mas'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scutil
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-20 00:00:00.000000000 Z
12
+ date: 2012-02-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-ssh
16
- requirement: &20792060 !ruby/object:Gem::Requirement
16
+ requirement: &11786280 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: 2.1.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *20792060
24
+ version_requirements: *11786280
25
25
  description: ! " Scutil is a library for conveniently executing commands \n remotely
26
26
  via SSH.\n"
27
27
  email: marcantoniosr@gmail.com
@@ -67,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
67
  version: '0'
68
68
  requirements: []
69
69
  rubyforge_project:
70
- rubygems_version: 1.8.11
70
+ rubygems_version: 1.8.17
71
71
  signing_key:
72
72
  specification_version: 3
73
73
  summary: SSH Command UTILity