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 +5 -0
- data/README.rdoc +36 -40
- data/lib/scutil/exec.rb +3 -4
- data/lib/scutil/system_connection.rb +10 -9
- data/lib/scutil.rb +151 -78
- data/scutil.gemspec +2 -2
- data/test/test_scutil.rb +68 -12
- metadata +5 -5
data/CHANGELOG.rdoc
CHANGED
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
|
-
|
8
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
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', '
|
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)
|
83
|
+
Copyright (c) 2012 by Marc Soda
|
87
84
|
|
88
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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,
|
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,
|
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
|
-
|
55
|
-
options.
|
56
|
-
|
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)
|
5
|
+
Copyright (C) 2012 by Marc Soda
|
6
6
|
|
7
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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.
|
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
|
-
#
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
#
|
81
|
-
#
|
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
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
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
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
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
|
-
#
|
98
|
-
#
|
99
|
-
#
|
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
|
104
|
-
# * :scutil_force_pty
|
105
|
-
# * :scutil_pty_regex
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
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("
|
145
|
+
raise Scutil::Error.new("Authenication failed for user: #{username}", hostname)
|
138
146
|
rescue SocketError => err
|
139
|
-
raise Scutil::Error.new(
|
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("
|
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,
|
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
|
-
|
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
|
-
|
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.
|
5
|
-
s.date = '
|
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 =>
|
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
|
-
|
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
|
-
|
208
|
-
|
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
|
-
|
214
|
-
|
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.
|
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:
|
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: &
|
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: *
|
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.
|
70
|
+
rubygems_version: 1.8.17
|
71
71
|
signing_key:
|
72
72
|
specification_version: 3
|
73
73
|
summary: SSH Command UTILity
|