rye 0.5.4 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +40 -21
- data/README.rdoc +28 -7
- data/bin/rye +1 -3
- data/bin/try +38 -8
- data/lib/rye.rb +22 -2
- data/lib/rye/box.rb +259 -77
- data/lib/rye/cmd.rb +86 -20
- data/lib/rye/rap.rb +23 -2
- data/lib/rye/set.rb +3 -3
- data/lib/sys.rb +13 -0
- data/rye.gemspec +10 -8
- data/tst/{50_rye_test.rb → 50_rset_test.rb} +1 -1
- data/tst/60-file.mp3 +0 -0
- data/tst/60_rbox_transfer_test.rb +53 -0
- data/tst/70_rbox_env_test.rb +19 -0
- metadata +6 -3
data/CHANGES.txt
CHANGED
@@ -5,6 +5,27 @@ TODO
|
|
5
5
|
* Fingerprints: ssh-keygen -l -f id_rsa_repos.pub
|
6
6
|
|
7
7
|
|
8
|
+
#### 0.6.0 (2009-04-28) #############################
|
9
|
+
|
10
|
+
* FIXED: handling of Process::Status ($?) in Rye.shell
|
11
|
+
* FIXED: Removed reference to Rudy::Error in bin/rye
|
12
|
+
* ADDED: Rye::Box.getenv
|
13
|
+
* ADDED: Rye::Box.can?
|
14
|
+
* ADDED: Rye::Box.umask= (a similar work around as cd / [])
|
15
|
+
* ADDED: Rye::Box.file_exists?
|
16
|
+
* ADDED: Rye::Box.authorize_keys_remote can now authorize for a different user
|
17
|
+
* ADDED: Rye::Box.upload and Rye::Box.download
|
18
|
+
* ADDED: Rye::SystemInfo.tmpdir
|
19
|
+
* CHANGE: Rye::Box.prep_args doesn't treat multicharacter Symbols as
|
20
|
+
command-line options any more (single character ones are still converted)
|
21
|
+
* CHANGE: Renamed Rye::Box.add_env to Rye::Box.setenv
|
22
|
+
* CHANGE: rm and kill are available in Rye::Cmd by default
|
23
|
+
* CHANGE: Rye::Box.authorize_keys renamed Rye::Box.authorize_keys_remote
|
24
|
+
* CHANGE: The Rye::Rap object now contains the exit code as an integer for
|
25
|
+
Rye.shell and Rye::Box.run_command (SSH) commands.
|
26
|
+
* UPDATED: Rdocs and README
|
27
|
+
|
28
|
+
|
8
29
|
#### 0.5.4 (2009-04-22) #############################
|
9
30
|
|
10
31
|
* FIXED: Sys is now returning environment paths and home path in JRuby.
|
@@ -19,57 +40,55 @@ TODO
|
|
19
40
|
* ADDED: Rye::Box.connect now rescues HostKeyMismatch exceptions and
|
20
41
|
prompts for a response.
|
21
42
|
|
22
|
-
|
23
43
|
#### 0.5.2 (2009-04-19) #############################
|
24
44
|
|
25
45
|
* FIXED: authorize-local command attempted to connect via SSH before authorizing.
|
26
46
|
|
27
47
|
#### 0.5.0 (2009-04-18) #############################
|
28
48
|
|
29
|
-
* ADDED: Rye::Box.switch_user
|
30
|
-
* ADDED: Several new commands to Rye::Cmd
|
31
|
-
* ADDED: Rye::Box.authorize_keys_local and "rye authorize-local
|
32
49
|
* FIXED: Bug in connect which prevented key-based logins for reconnections
|
33
50
|
* FIXED: Method errors in JRuby
|
34
51
|
* FIXED: Bug in Rye::Set.add_boxes pushing nils into the list of boxes
|
52
|
+
* ADDED: Rye::Box.switch_user
|
53
|
+
* ADDED: Several new commands to Rye::Cmd
|
54
|
+
* ADDED: Rye::Box.authorize_keys_local and "rye authorize-local
|
55
|
+
|
35
56
|
|
36
57
|
#### 0.4.3 (2009-04-14) #############################
|
37
58
|
|
38
|
-
* ADDED: Rye::Box.missing_method to handle non existent commands
|
39
59
|
* FIXED: All Rye::Cmd command methods accept *args to make calling consistent.
|
40
|
-
|
60
|
+
* ADDED: Rye::Box.missing_method to handle non existent commands
|
41
61
|
|
42
62
|
#### 0.4.2 (2009-04-13) #############################
|
43
63
|
|
44
64
|
* ADDED: More helpful debug output
|
45
65
|
* ADDED: hostname command to Rye::Cmd
|
46
|
-
* CHANGE: Using OpenSSL's ssh-agent but also let's Net::SSH handle
|
47
|
-
the ssh keys.
|
48
66
|
* ADDED: Rye::Box.connect now supports multiple password attempts if STDIN.tty returns true
|
49
67
|
* ADDED: Rye::Box.interactive_ssh for opening an SSH session to the given box.
|
68
|
+
* CHANGE: Using OpenSSL's ssh-agent but also let's Net::SSH handle the ssh keys.
|
50
69
|
|
51
70
|
|
52
71
|
#### 0.4.1 (2009-04-06) #############################
|
53
72
|
|
54
|
-
* FIXED: Rye::Box.
|
55
|
-
* ADDED: "rye authorize" now specifically enforces the auth method order
|
73
|
+
* FIXED: Rye::Box.authorize_keys_remote was not disabling safe mode properly
|
56
74
|
* FIXED: Disabled debug mode.
|
75
|
+
* ADDED: "rye authorize" now specifically enforces the auth method order
|
57
76
|
|
58
77
|
|
59
78
|
#### 0.4.0 (2009-04-06) #############################
|
60
79
|
|
61
|
-
* ADDED: to_s and inspect methods for cleaner debugging output
|
62
|
-
* ADDED: == method for Rye::Box
|
63
|
-
* ADDED: exit code and exit signal to Rye::Rap objects
|
64
|
-
* ADDED: commands now raise a Rye::CommandError exception
|
65
|
-
when the command returns an exit code greater than 0.
|
66
|
-
* CHANGED: Box.add_command renamed to Box.run_command
|
67
80
|
* FIXED: Box.run_command was parsing arguments incorrectly
|
68
81
|
* FIXED: Box.net_ssh_exec was working on nil stderr
|
69
82
|
* FIXED: bin/try handles the new command exceptions
|
83
|
+
* ADDED: to_s and inspect methods for cleaner debugging output
|
84
|
+
* ADDED: == method for Rye::Box
|
85
|
+
* ADDED: exit code and exit signal to Rye::Rap objects
|
70
86
|
* ADDED: Command switches can now be sent as Symbols (rbox.ls(:h))
|
71
87
|
* ADDED: Rye.host_keys
|
72
88
|
* ADDED: bin/rye
|
89
|
+
* ADDED: commands now raise a Rye::CommandError exception
|
90
|
+
when the command returns an exit code greater than 0.
|
91
|
+
* CHANGE: Box.add_command renamed to Box.run_command
|
73
92
|
|
74
93
|
|
75
94
|
#### 0.3.2 (2009-04-05) #############################
|
@@ -81,24 +100,24 @@ TODO
|
|
81
100
|
|
82
101
|
#### 0.3 (2009-04-05) ###############################
|
83
102
|
|
103
|
+
* FIXED: Rye::Box wasn't properly adding keypairs to SSH Agent
|
104
|
+
* FIXED: Rye::Box.method_missing Symbol/String ambiguity
|
84
105
|
* ADDED: Rye::Set supports executing commands parallel
|
85
106
|
* ADDED: Rye::Rap now contains STDERR output from command
|
86
|
-
* FIXED: Rye::Box wasn't properly adding keypairs to SSH Agent
|
87
|
-
* CHANGED: Moved all SSH key stuff to Rye (used to be done per Box)
|
88
107
|
* ADDED: Supports all options provided by Net::SSH#start. This
|
89
|
-
includes support for password logins and proxies.
|
108
|
+
includes support for password logins and proxies.
|
90
109
|
* ADDED: Safe mode can now be disabled (to allow file globs
|
91
110
|
and environment variable access).
|
92
111
|
* ADDED: Basic sanity test
|
93
|
-
* FIXED: Rye::Box.method_missing Symbol/String ambiguity
|
94
112
|
* ADDED: Mucho more rdocs and examples.
|
113
|
+
* CHANGE: Moved all SSH key stuff to Rye (used to be done per Box)
|
114
|
+
|
95
115
|
|
96
116
|
#### 0.2 (2009-04-04) ###############################
|
97
117
|
|
98
118
|
* FIXED: ssh-agent shutdown wasn't deleting the SSH tmp directory
|
99
119
|
* ADDED: Now with more rdocs!
|
100
120
|
|
101
|
-
|
102
121
|
#### 0.1 (2009-04-03) ###############################
|
103
122
|
|
104
123
|
Initial public release
|
data/README.rdoc
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
= Rye - v0.
|
1
|
+
= Rye - v0.6
|
2
2
|
|
3
3
|
Safely run SSH commands on a bunch of machines at the same time (from Ruby).
|
4
4
|
|
@@ -38,7 +38,7 @@ One of:
|
|
38
38
|
puts rbox.cd('/home/rye').pwd # => /home/rye
|
39
39
|
|
40
40
|
# You can specify environment variables
|
41
|
-
rbox.
|
41
|
+
rbox.setenv(:RYE, "Forty Creek")
|
42
42
|
rbox.env # => ['HOME=/home/rye', 'RYE=Forty Creek', ...]
|
43
43
|
|
44
44
|
# The commands method returns an Array of available commands:
|
@@ -123,7 +123,7 @@ One of:
|
|
123
123
|
puts unames.first.box == rbox # => true
|
124
124
|
|
125
125
|
# Envrionment variables can be set the same way as with Rye::Box
|
126
|
-
rset.
|
126
|
+
rset.setenv(:RYE, "Forty Creek")
|
127
127
|
p rset.env.first.select { |env| env =~ /RYE/ } # => ["RYE=Forty Creek"]
|
128
128
|
|
129
129
|
|
@@ -152,20 +152,41 @@ One of:
|
|
152
152
|
# There were no actual error so the exit code should be 0.
|
153
153
|
puts rbox.uname('-a 1>&2').exit_code # => 0
|
154
154
|
|
155
|
-
|
155
|
+
|
156
|
+
== EXAMPLE 6 -- FILE TRANSFERS
|
157
|
+
|
158
|
+
rbox = Rye::Box.new("localhost", :info => true)
|
159
|
+
|
160
|
+
dir_upload = "#{Rye.sysinfo.tmpdir}/rye-upload/"
|
161
|
+
dir_download = "#{Rye.sysinfo.tmpdir}/rye-download/"
|
162
|
+
|
163
|
+
rbox.upload("#{RYE_HOME}/README.rdoc",
|
164
|
+
"#{RYE_HOME}/LICENSE.txt", dir_upload)
|
165
|
+
|
166
|
+
applejack = StringIO.new("Some in-memory content")
|
167
|
+
rbox.upload(applejack, "#{dir_upload}/applejack.txt")
|
168
|
+
|
169
|
+
p rbox.ls(dir_upload) # => [README.rdoc, LICENSE.txt, applejack.txt]
|
170
|
+
p rbox.cat("#{dir_upload}/applejack.txt") # => "Some in-memory content"
|
171
|
+
|
172
|
+
filecontent = StringIO.new
|
173
|
+
rbox.download("#{dir_upload}/applejack.txt", filecontent)
|
174
|
+
|
175
|
+
p filecontent.read
|
176
|
+
|
177
|
+
== EXAMPLE 7 -- rye
|
156
178
|
|
157
179
|
Rye comes with a command-line utility called <tt>rye</tt>.
|
158
180
|
|
159
181
|
# Prints the paths to your private keys
|
160
182
|
$ rye keys
|
161
|
-
|
183
|
+
|
162
184
|
# Prints the server host keys (suitable for ~/.ssh/known_hosts)
|
163
185
|
$ rye hostkey HOST1 HOST2
|
164
|
-
|
186
|
+
|
165
187
|
# Add your public keys to authorized_keys and authorized_keys2
|
166
188
|
# on a remote machine
|
167
189
|
$ rye authorize HOST1 HOST2
|
168
|
-
|
169
190
|
More info:
|
170
191
|
|
171
192
|
$ rye -h
|
data/bin/rye
CHANGED
@@ -48,7 +48,7 @@ command :authorize do |obj|
|
|
48
48
|
|
49
49
|
puts "Authorizing #{rbox.opts[:user]}@#{hostname}"
|
50
50
|
rbox = Rye::Box.new(hostname, opts).connect
|
51
|
-
puts "Added public keys for: ", rbox.
|
51
|
+
puts "Added public keys for: ", rbox.authorize_keys_remote
|
52
52
|
puts "Now try: " << "ssh #{rbox.opts[:user]}@#{hostname}"
|
53
53
|
|
54
54
|
end
|
@@ -118,8 +118,6 @@ begin
|
|
118
118
|
rescue Drydock::ArgError, Drydock::OptError=> ex
|
119
119
|
STDERR.puts ex.message
|
120
120
|
STDERR.puts ex.usage
|
121
|
-
rescue Rudy::Error => ex
|
122
|
-
STDERR.puts ex.message
|
123
121
|
rescue => ex
|
124
122
|
STDERR.puts "ERROR (#{ex.class.to_s}): #{ex.message}"
|
125
123
|
STDERR.puts ex.backtrace if Drydock.debug?
|
data/bin/try
CHANGED
@@ -8,7 +8,8 @@
|
|
8
8
|
# Usage: bin/try
|
9
9
|
#
|
10
10
|
|
11
|
-
|
11
|
+
RYE_HOME = File.join(File.dirname(__FILE__), '..')
|
12
|
+
$:.unshift File.join(RYE_HOME, 'lib')
|
12
13
|
|
13
14
|
require 'stringio'
|
14
15
|
require 'yaml'
|
@@ -34,10 +35,9 @@ p rbox.ls(:a, '/') # => ['.', '..', 'bin', 'etc', ...]
|
|
34
35
|
puts rbox.pwd # => /home/rye
|
35
36
|
puts rbox['/usr/bin'].pwd # => /usr/bin
|
36
37
|
puts rbox.pwd # => /usr/bin
|
37
|
-
puts rbox.cd(Rye.sysinfo.home).pwd # => /home/rye
|
38
38
|
|
39
39
|
# You can specify environment variables
|
40
|
-
rbox.
|
40
|
+
rbox.setenv(:RYE, "Forty Creek")
|
41
41
|
rbox.env # => ['HOME=/home/rye', 'RYE=Forty Creek', ...]
|
42
42
|
|
43
43
|
# The commands method returns an Array of available commands:
|
@@ -133,7 +133,7 @@ puts unames.first.box.class # => Rye::Box
|
|
133
133
|
puts unames.first.box == rbox # => true
|
134
134
|
|
135
135
|
# Envrionment variables can be set the same way as with Rye::Box
|
136
|
-
rset.
|
136
|
+
rset.setenv(:RYE, "Forty Creek")
|
137
137
|
p rset.env.first.select { |env| env =~ /RYE/ } # => ["RYE=Forty Creek"]
|
138
138
|
|
139
139
|
|
@@ -142,7 +142,7 @@ puts %Q(
|
|
142
142
|
# EXAMPLE 5 -- ERROR HANDLING
|
143
143
|
#)
|
144
144
|
|
145
|
-
rbox = Rye::Box.new('localhost', :safe => false)
|
145
|
+
rbox = Rye::Box.new('localhost', :safe => false) # Note: safe mode is off
|
146
146
|
|
147
147
|
# Rye follows the standard convention of taking exception to a non-zero
|
148
148
|
# exit code by raising a Rye::CommandError. In this case, rye9000.test
|
@@ -150,8 +150,8 @@ rbox = Rye::Box.new('localhost', :safe => false)
|
|
150
150
|
begin
|
151
151
|
rbox.ls('rye.test')
|
152
152
|
rescue Rye::CommandError => ex
|
153
|
-
puts ex.exit_code
|
154
|
-
puts ex.stderr
|
153
|
+
puts ex.exit_code # => 1
|
154
|
+
puts ex.stderr # => ls: rye.test: No such file or directory
|
155
155
|
end
|
156
156
|
|
157
157
|
# The Rye:Rap response objects also give you the STDOUT and STDERR
|
@@ -162,10 +162,40 @@ puts rbox.uname('-a 1>&2').stdout # =>
|
|
162
162
|
# It all went to STDERR:
|
163
163
|
puts rbox.uname('-a 1>&2').stderr # => Darwin ryehost 9.6.0 ...
|
164
164
|
|
165
|
-
# There were no actual
|
165
|
+
# There were no actual errors so the exit code should be 0.
|
166
166
|
puts rbox.uname('-a 1>&2').exit_code # => 0
|
167
167
|
|
168
168
|
|
169
|
+
puts %Q(
|
170
|
+
# ------------------------------------------------------------------
|
171
|
+
# EXAMPLE 6 -- FILE TRANSFERS
|
172
|
+
#)
|
173
|
+
|
174
|
+
dir_upload = "#{Rye.sysinfo.tmpdir}/rye-upload/"
|
175
|
+
dir_download = "#{Rye.sysinfo.tmpdir}/rye-download/"
|
176
|
+
|
177
|
+
rbox = Rye::Box.new("localhost", :info => false)
|
178
|
+
|
179
|
+
# Rye ships without an rm method (for safety!). Here
|
180
|
+
# we add the rm method only to this instance of rbox.
|
181
|
+
def rbox.rm(*args); cmd('rm', args); end
|
182
|
+
|
183
|
+
rbox.rm(:r, :f, dir_upload) # Silently delete test dirs
|
184
|
+
rbox.rm(:r, :f, dir_download)
|
185
|
+
|
186
|
+
rbox.upload("#{RYE_HOME}/README.rdoc",
|
187
|
+
"#{RYE_HOME}/LICENSE.txt", dir_upload)
|
188
|
+
|
189
|
+
applejack = StringIO.new("Some in-memory content")
|
190
|
+
rbox.upload(applejack, "#{dir_upload}/applejack.txt")
|
191
|
+
|
192
|
+
p rbox.ls(dir_upload) # => [README.rdoc, LICENSE.txt, applejack.txt]
|
193
|
+
p rbox.cat("#{dir_upload}/applejack.txt") # => "Some in-memory content"
|
194
|
+
|
195
|
+
filecontent = StringIO.new
|
196
|
+
rbox.download("#{dir_upload}/applejack.txt", filecontent)
|
197
|
+
|
198
|
+
p filecontent.read
|
169
199
|
|
170
200
|
|
171
201
|
|
data/lib/rye.rb
CHANGED
@@ -36,7 +36,7 @@ module Rye
|
|
36
36
|
extend self
|
37
37
|
|
38
38
|
unless defined?(SYSINFO)
|
39
|
-
VERSION = 0.
|
39
|
+
VERSION = 0.6.freeze
|
40
40
|
SYSINFO = SystemInfo.new.freeze
|
41
41
|
end
|
42
42
|
|
@@ -54,6 +54,7 @@ module Rye
|
|
54
54
|
class NotConnected < RuntimeError; end
|
55
55
|
class CommandNotFound < RuntimeError; end
|
56
56
|
class CommandError < RuntimeError
|
57
|
+
attr_reader :rap
|
57
58
|
# * +rap+ a Rye::Rap object
|
58
59
|
def initialize(rap)
|
59
60
|
@rap = rap
|
@@ -208,7 +209,7 @@ module Rye
|
|
208
209
|
rap = Rye::Rap.new(self)
|
209
210
|
rap.add_stdout(stdout || '')
|
210
211
|
rap.add_stderr(stderr || '')
|
211
|
-
rap.
|
212
|
+
rap.add_exit_code($?)
|
212
213
|
rap
|
213
214
|
end
|
214
215
|
|
@@ -224,6 +225,25 @@ module Rye
|
|
224
225
|
@@agent_env
|
225
226
|
end
|
226
227
|
|
228
|
+
# Returns +str+ with the leading indentation removed.
|
229
|
+
# Stolen from http://github.com/mynyml/unindent/ because it was better.
|
230
|
+
def without_indent(str)
|
231
|
+
indent = str.split($/).each {|line| !line.strip.empty? }.map {|line| line.index(/[^\s]/) }.compact.min
|
232
|
+
str.gsub(/^[[:blank:]]{#{indent}}/, '')
|
233
|
+
end
|
234
|
+
|
235
|
+
#
|
236
|
+
# Generates a string of random alphanumeric characters.
|
237
|
+
# * +len+ is the length, an Integer. Default: 8
|
238
|
+
# * +safe+ in safe-mode, ambiguous characters are removed (default: true):
|
239
|
+
# i l o 1 0
|
240
|
+
def strand( len=8, safe=true )
|
241
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
242
|
+
chars.delete_if { |v| %w(i l o 1 0).member?(v) } if safe
|
243
|
+
str = ""
|
244
|
+
1.upto(len) { |i| str << chars[rand(chars.size-1)] }
|
245
|
+
str
|
246
|
+
end
|
227
247
|
|
228
248
|
private
|
229
249
|
|
data/lib/rye/box.rb
CHANGED
@@ -19,22 +19,32 @@ module Rye
|
|
19
19
|
# rbox.hostname # => localhost
|
20
20
|
# rbox.uname(:a) # => Darwin vanya 9.6.0 ...
|
21
21
|
#
|
22
|
+
#--
|
23
|
+
# * When anything confusing happens, enable debug in initialize
|
24
|
+
# by passing :debug => STDERR. This will output Rye debug info
|
25
|
+
# as well as Net::SSH info. This is VERY helpful for figuring
|
26
|
+
# out why some command is hanging or otherwise acting weird.
|
27
|
+
# * If a remote command is hanging, it's probably because a
|
28
|
+
# Net::SSH channel is waiting on_extended_data (a prompt).
|
29
|
+
#++
|
22
30
|
class Box
|
23
31
|
include Rye::Cmd
|
24
32
|
|
25
33
|
# An instance of Net::SSH::Connection::Session
|
26
34
|
attr_reader :ssh
|
27
35
|
|
36
|
+
attr_reader :info
|
28
37
|
attr_reader :debug
|
29
38
|
attr_reader :error
|
30
39
|
|
31
40
|
attr_accessor :host
|
32
|
-
|
33
41
|
attr_accessor :safe
|
34
42
|
attr_accessor :opts
|
35
|
-
|
43
|
+
|
36
44
|
# The most recent value from Box.cd or Box.[]
|
37
45
|
attr_reader :current_working_directory
|
46
|
+
# The most recent valud for umask (or 0022)
|
47
|
+
attr_reader :current_umask
|
38
48
|
|
39
49
|
# * +host+ The hostname to connect to. The default is localhost.
|
40
50
|
# * +opts+ a hash of optional arguments.
|
@@ -44,14 +54,17 @@ module Rye
|
|
44
54
|
# * :user => the username to connect as. Default: the current user.
|
45
55
|
# * :safe => should Rye be safe? Default: true
|
46
56
|
# * :keys => one or more private key file paths (passwordless login)
|
47
|
-
# * :
|
57
|
+
# * :info => an IO object to print Rye::Box command info to. Default: nil
|
48
58
|
# * :debug => an IO object to print Rye::Box debugging info to. Default: nil
|
49
59
|
# * :error => an IO object to print Rye::Box errors to. Default: STDERR
|
60
|
+
# * :getenv => pre-fetch +host+ environment variables? (default: true)
|
61
|
+
# * :password => the user's password (ignored if there's a valid private key)
|
50
62
|
#
|
51
63
|
# NOTE: +opts+ can also contain any parameter supported by
|
52
64
|
# Net::SSH.start that is not already mentioned above.
|
53
65
|
#
|
54
66
|
def initialize(host='localhost', opts={})
|
67
|
+
@host = host
|
55
68
|
|
56
69
|
# These opts are use by Rye::Box and also passed to Net::SSH
|
57
70
|
@opts = {
|
@@ -59,48 +72,46 @@ module Rye
|
|
59
72
|
:safe => true,
|
60
73
|
:port => 22,
|
61
74
|
:keys => [],
|
75
|
+
:info => nil,
|
62
76
|
:debug => nil,
|
63
77
|
:error => STDERR,
|
78
|
+
:getenv => true,
|
64
79
|
}.merge(opts)
|
65
80
|
|
66
|
-
# See Net::SSH.start
|
67
|
-
@opts[:paranoid] = true unless @opts[:safe] == false
|
68
|
-
|
69
81
|
# Close the SSH session before Ruby exits. This will do nothing
|
70
82
|
# if disconnect has already been called explicitly.
|
71
|
-
at_exit {
|
72
|
-
self.disconnect
|
73
|
-
}
|
74
|
-
|
75
|
-
@host = host
|
83
|
+
at_exit { self.disconnect }
|
76
84
|
|
77
|
-
@
|
78
|
-
|
79
|
-
@
|
85
|
+
# @opts gets sent to Net::SSH so we need to remove the keys
|
86
|
+
# that are not meant for it.
|
87
|
+
@safe, @debug = @opts.delete(:safe), @opts.delete(:debug)
|
88
|
+
@info, @error = @opts.delete(:info), @opts.delete(:error)
|
89
|
+
@getenv = {} if @opts.delete(:getenv) # Enable getenv with a hash
|
80
90
|
|
81
|
-
|
82
|
-
|
91
|
+
# Just in case someone sends a true value rather than IO object
|
92
|
+
@debug = STDERR if @debug == true
|
93
|
+
@error = STDERR if @error == true
|
94
|
+
@info = STDOUT if @info == true
|
95
|
+
|
96
|
+
@opts[:logger] = Logger.new(@debug) if @debug # Enable Net::SSH debugging
|
97
|
+
@opts[:paranoid] = true unless @opts[:safe] == false # See Net::SSH.start
|
98
|
+
|
99
|
+
# Add the given private keys to the keychain that will be used for @host
|
83
100
|
add_keys(@opts[:keys])
|
84
101
|
|
85
102
|
# We don't want Net::SSH to handle the keypairs. This may change
|
86
103
|
# but for we're letting ssh-agent do it.
|
87
104
|
#@opts.delete(:keys)
|
88
105
|
|
89
|
-
|
90
|
-
|
106
|
+
# From: capistrano/lib/capistrano/cli.rb
|
107
|
+
STDOUT.sync = true # so that Net::SSH prompts show up
|
91
108
|
|
109
|
+
debug "ssh-agent info: #{Rye.sshagent_info.inspect}"
|
110
|
+
debug @opts.inspect
|
111
|
+
|
92
112
|
end
|
93
113
|
|
94
|
-
|
95
|
-
# Returns an Array of system commands available over SSH
|
96
|
-
def can
|
97
|
-
Rye::Cmd.instance_methods
|
98
|
-
end
|
99
|
-
alias :commands :can
|
100
|
-
alias :cmds :can
|
101
114
|
|
102
|
-
|
103
|
-
|
104
115
|
# Change the current working directory (sort of).
|
105
116
|
#
|
106
117
|
# I haven't been able to wrangle Net::SSH to do my bidding.
|
@@ -123,19 +134,33 @@ module Rye
|
|
123
134
|
@current_working_directory = key
|
124
135
|
self
|
125
136
|
end
|
126
|
-
#
|
127
|
-
|
137
|
+
# Like [] except it returns an empty Rye::Rap object to mimick
|
138
|
+
# a regular command method. Call with nil key (or no arg) to
|
139
|
+
# reset.
|
140
|
+
def cd(key=nil)
|
128
141
|
@current_working_directory = key
|
142
|
+
ret = Rye::Rap.new(self)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Change the current umask (sort of -- works the same way as cd)
|
146
|
+
# The default umask is 0022
|
147
|
+
def umask=(val='0022')
|
148
|
+
@current_umask = val
|
129
149
|
self
|
130
150
|
end
|
131
|
-
|
151
|
+
|
152
|
+
|
132
153
|
# Open an SSH session with +@host+. This called automatically
|
133
154
|
# when you the first comamnd is run if it's not already connected.
|
134
155
|
# Raises a Rye::NoHost exception if +@host+ is not specified.
|
135
156
|
# Will attempt a password login up to 3 times if the initial
|
136
157
|
# authentication fails.
|
137
|
-
|
158
|
+
# * +reconnect+ Disconnect first if already connected. The default
|
159
|
+
# is true. When set to false, connect will do nothing if already
|
160
|
+
# connected.
|
161
|
+
def connect(reconnect=true)
|
138
162
|
raise Rye::NoHost unless @host
|
163
|
+
return if @ssh && !reconnect
|
139
164
|
disconnect if @ssh
|
140
165
|
debug "Opening connection to #{@host} as #{@opts[:user]}"
|
141
166
|
highline = HighLine.new # Used for password prompt
|
@@ -146,7 +171,7 @@ module Rye
|
|
146
171
|
rescue Net::SSH::HostKeyMismatch => ex
|
147
172
|
STDERR.puts ex.message
|
148
173
|
STDERR.puts "NOTE: EC2 instances generate new SSH keys on first boot."
|
149
|
-
print "\a" # Ring the bell
|
174
|
+
print "\a" if @info # Ring the bell
|
150
175
|
if highline.ask("Continue? ").strip.match(/\Ay|yes|sure|ya\z/i)
|
151
176
|
@opts[:paranoid] = false
|
152
177
|
retry
|
@@ -154,6 +179,7 @@ module Rye
|
|
154
179
|
raise Net::SSH::HostKeyMismatch
|
155
180
|
end
|
156
181
|
rescue Net::SSH::AuthenticationFailed => ex
|
182
|
+
print "\a" if retried == 0 && @info # Ring the bell once
|
157
183
|
retried += 1
|
158
184
|
if STDIN.tty? && retried <= 3
|
159
185
|
@opts[:password] = highline.ask("Password: ") { |q| q.echo = '' }
|
@@ -171,20 +197,11 @@ module Rye
|
|
171
197
|
# the next connection (if we switch_user is called for example).
|
172
198
|
@opts.delete :auth_methods if @opts.has_key?(:auth_methods)
|
173
199
|
|
174
|
-
@ssh.is_a?(Net::SSH::Connection::Session) && !@ssh.closed?
|
175
200
|
self
|
176
201
|
end
|
177
202
|
|
178
|
-
#
|
179
|
-
#
|
180
|
-
def disconnect
|
181
|
-
return unless @ssh && !@ssh.closed?
|
182
|
-
@ssh.loop(0.1) { @ssh.busy? }
|
183
|
-
debug "Closing connection to #{@ssh.host}"
|
184
|
-
@ssh.close
|
185
|
-
end
|
186
|
-
|
187
|
-
# Reconnect as another user
|
203
|
+
# Reconnect as another user. This is different from su=
|
204
|
+
# which executes subsequent commands via +su -c COMMAND USER+.
|
188
205
|
# * +newuser+ The username to reconnect as
|
189
206
|
#
|
190
207
|
# NOTE: if there is an open connection, it's disconnected
|
@@ -197,6 +214,17 @@ module Rye
|
|
197
214
|
connect
|
198
215
|
end
|
199
216
|
|
217
|
+
|
218
|
+
# Close the SSH session with +@host+. This is called
|
219
|
+
# automatically at exit if the connection is open.
|
220
|
+
def disconnect
|
221
|
+
return unless @ssh && !@ssh.closed?
|
222
|
+
@ssh.loop(0.1) { @ssh.busy? }
|
223
|
+
debug "Closing connection to #{@ssh.host}"
|
224
|
+
@ssh.close
|
225
|
+
end
|
226
|
+
|
227
|
+
|
200
228
|
# Open an interactive SSH session. This only works if STDIN.tty?
|
201
229
|
# returns true. Otherwise it returns the SSH command that would
|
202
230
|
# have been run. This requires the SSH command-line executable (ssh).
|
@@ -229,12 +257,14 @@ module Rye
|
|
229
257
|
|
230
258
|
# Add an environment variable. +n+ and +v+ are the name and value.
|
231
259
|
# Returns the instance of Rye::Box
|
232
|
-
def
|
233
|
-
debug "
|
260
|
+
def setenv(n, v)
|
261
|
+
debug "Adding env: #{n}=#{v}"
|
262
|
+
debug "prev value: #{@getenv[n]}"
|
263
|
+
@getenv[n] = v
|
234
264
|
(@current_environment_variables ||= {})[n] = v
|
235
265
|
self
|
236
266
|
end
|
237
|
-
alias :
|
267
|
+
alias :add_env :setenv # deprecated?
|
238
268
|
|
239
269
|
def user
|
240
270
|
(@opts || {})[:user]
|
@@ -251,9 +281,10 @@ module Rye
|
|
251
281
|
end
|
252
282
|
|
253
283
|
def inspect
|
254
|
-
%q{#<%s:%s cwd=%s env=%s safe=%s opts=%s>} %
|
284
|
+
%q{#<%s:%s cwd=%s umask=%s env=%s safe=%s opts=%s>} %
|
255
285
|
[self.class.to_s, self.host,
|
256
|
-
@current_working_directory,
|
286
|
+
@current_working_directory, @current_umask,
|
287
|
+
(@current_environment_variables || '').inspect,
|
257
288
|
self.safe, self.opts.inspect]
|
258
289
|
end
|
259
290
|
|
@@ -269,34 +300,89 @@ module Rye
|
|
269
300
|
Rye.remote_host_keys(@host)
|
270
301
|
end
|
271
302
|
|
303
|
+
# Uses the output of "useradd -D" to determine the default home
|
304
|
+
# directory. This returns a GUESS rather than the a user's real
|
305
|
+
# home directory. Currently used only by authorize_keys_remote.
|
306
|
+
def guess_user_home(other_user=nil)
|
307
|
+
# Some junk to determine where user home directories are by default.
|
308
|
+
# We're relying on the command "useradd -D" so this may not work on
|
309
|
+
# different Linuxen and definitely won't work on Windows.
|
310
|
+
# This code will be abstracted out once I find a decent home for it.
|
311
|
+
# /etc/default/useradd, HOME=/home OR useradd -D
|
312
|
+
# /etc/adduser.config, DHOME=/home OR ??
|
313
|
+
user_defaults = {}
|
314
|
+
raw = self.useradd(:D) rescue ["HOME=/home"]
|
315
|
+
raw.each do |nv|
|
316
|
+
n, v = nv.scan(/\A([\w_-]+?)=(.+)\z/).flatten
|
317
|
+
user_defaults[n] = v
|
318
|
+
end
|
319
|
+
"#{user_defaults['HOME']}/#{other_user}"
|
320
|
+
end
|
321
|
+
|
272
322
|
# Copy the local public keys (as specified by Rye.keys) to
|
273
323
|
# this box into ~/.ssh/authorized_keys and ~/.ssh/authorized_keys2.
|
274
|
-
# Returns
|
275
|
-
#
|
276
|
-
#
|
324
|
+
# Returns a Rye::Rap object. The private keys files used to generate
|
325
|
+
# the public keys are contained in stdout.
|
326
|
+
# Raises a Rye::ComandError if the home directory doesn't exit.
|
327
|
+
# NOTE: authorize_keys_remote disables safe-mode for this box while it runs
|
277
328
|
# which will hit you funky style if your using a single instance
|
278
329
|
# of Rye::Box in a multithreaded situation.
|
279
330
|
#
|
280
|
-
def
|
331
|
+
def authorize_keys_remote(other_user=nil)
|
332
|
+
this_user = other_user || @user
|
281
333
|
added_keys = []
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
334
|
+
rap = Rye::Rap.new(self)
|
335
|
+
|
336
|
+
# The homedir path is important b/c this is where we're going to
|
337
|
+
# look for the .ssh directory. That's where auth love is stored.
|
338
|
+
homedir = self.guess_user_home(this_user)
|
339
|
+
|
340
|
+
unless self.file_exists?(homedir)
|
341
|
+
rap.add_exit_code(1)
|
342
|
+
rap.add_stderr("Path does not exist: #{homedir}")
|
343
|
+
raise Rye::CommandError.new(rap)
|
292
344
|
end
|
293
|
-
|
294
|
-
|
345
|
+
|
346
|
+
# Let's go into the user's home directory that we now know exists.
|
347
|
+
self.cd homedir
|
348
|
+
|
349
|
+
files = ['.ssh/authorized_keys', '.ssh/authorized_keys2']
|
350
|
+
files.each do |akey_path|
|
351
|
+
if self.file_exists?(akey_path)
|
352
|
+
# TODO: Make Rye::Cmd.incremental_backup
|
353
|
+
self.cp(akey_path, "#{akey_path}-previous")
|
354
|
+
authorized_keys = self.download("#{homedir}/#{akey_path}")
|
355
|
+
end
|
356
|
+
authorized_keys ||= StringIO.new
|
357
|
+
|
358
|
+
Rye.keys.each do |key|
|
359
|
+
path = key[2]
|
360
|
+
info "# Adding public key for #{path}"
|
361
|
+
k = Rye::Key.from_file(path).public_key.to_ssh2
|
362
|
+
authorized_keys.puts k
|
363
|
+
end
|
364
|
+
|
365
|
+
# Remove duplicate authorized keys
|
366
|
+
authorized_keys.rewind
|
367
|
+
uniqlines = authorized_keys.readlines.uniq.join
|
368
|
+
authorized_keys = StringIO.new(uniqlines)
|
369
|
+
# We need to rewind so that all of the StringIO object is uploaded
|
370
|
+
authorized_keys.rewind
|
371
|
+
|
372
|
+
self.mkdir(:p, :m, '700', File.dirname(akey_path))
|
373
|
+
self.upload(authorized_keys, "#{homedir}/#{akey_path}")
|
374
|
+
self.chmod('0600', akey_path)
|
375
|
+
self.chown(:R, this_user.to_s, File.dirname(akey_path))
|
376
|
+
end
|
377
|
+
|
378
|
+
|
379
|
+
rap.add_exit_code(0)
|
380
|
+
rap
|
295
381
|
end
|
296
382
|
|
297
383
|
# Authorize the current user to login to the local machine via
|
298
384
|
# SSH without a password. This is the same functionality as
|
299
|
-
#
|
385
|
+
# authorize_keys_remote except run with local shell commands.
|
300
386
|
def authorize_keys_local
|
301
387
|
added_keys = []
|
302
388
|
Rye.keys.each do |key|
|
@@ -323,10 +409,10 @@ module Rye
|
|
323
409
|
|
324
410
|
private
|
325
411
|
|
326
|
-
|
327
412
|
def debug(msg="unknown debug msg"); @debug.puts msg if @debug; end
|
328
413
|
def error(msg="unknown error msg"); @error.puts msg if @error; end
|
329
|
-
|
414
|
+
def pinfo(msg="unknown info msg"); @info.print msg if @info; end
|
415
|
+
def info(msg="unknown info msg"); @info.puts msg if @info; end
|
330
416
|
|
331
417
|
# Add the current environment variables to the beginning of +cmd+
|
332
418
|
def prepend_env(cmd)
|
@@ -348,7 +434,7 @@ module Rye
|
|
348
434
|
# will be thoroughly escaped and passed to the command.
|
349
435
|
#
|
350
436
|
# rbox = Rye::Box.new
|
351
|
-
# rbox.ls
|
437
|
+
# rbox.ls :l, 'arg1', 'arg2'
|
352
438
|
#
|
353
439
|
# is equivalent to
|
354
440
|
#
|
@@ -367,17 +453,30 @@ module Rye
|
|
367
453
|
|
368
454
|
cmd_clean = Rye.escape(@safe, cmd, args)
|
369
455
|
cmd_clean = prepend_env(cmd_clean)
|
456
|
+
|
457
|
+
# Add the current working directory before the command if supplied.
|
458
|
+
# The command will otherwise run in the user's home directory.
|
370
459
|
if @current_working_directory
|
371
460
|
cwd = Rye.escape(@safe, 'cd', @current_working_directory)
|
372
461
|
cmd_clean = [cwd, cmd_clean].join(' && ')
|
373
462
|
end
|
374
463
|
|
464
|
+
# ditto (same explanation as cwd)
|
465
|
+
if @current_umask
|
466
|
+
cwd = Rye.escape(@safe, 'umask', @current_umask)
|
467
|
+
cmd_clean = [cwd, cmd_clean].join(' && ')
|
468
|
+
end
|
469
|
+
|
470
|
+
|
471
|
+
info "COMMAND: #{cmd_clean}"
|
375
472
|
debug "Executing: %s" % cmd_clean
|
376
|
-
|
473
|
+
|
474
|
+
stdout, stderr, ecode, esignal = net_ssh_exec!(cmd_clean)
|
475
|
+
|
377
476
|
rap = Rye::Rap.new(self)
|
378
477
|
rap.add_stdout(stdout || '')
|
379
478
|
rap.add_stderr(stderr || '')
|
380
|
-
rap.
|
479
|
+
rap.add_exit_code(ecode)
|
381
480
|
rap.exit_signal = esignal
|
382
481
|
rap.cmd = cmd
|
383
482
|
|
@@ -387,10 +486,10 @@ module Rye
|
|
387
486
|
end
|
388
487
|
alias :cmd :run_command
|
389
488
|
|
390
|
-
|
391
|
-
|
392
489
|
# Takes a list of arguments appropriate for run_command or
|
393
|
-
# preview_command and returns: [cmd, args]
|
490
|
+
# preview_command and returns: [cmd, args].
|
491
|
+
# Single character symbols with be converted to command line
|
492
|
+
# switches. Example: +:l+ becomes +-l+
|
394
493
|
def prep_args(*args)
|
395
494
|
args = args.flatten.compact
|
396
495
|
args = args.first.to_s.split(/\s+/) if args.size == 1
|
@@ -398,8 +497,9 @@ module Rye
|
|
398
497
|
|
399
498
|
# Symbols to switches. :l -> -l, :help -> --help
|
400
499
|
args.collect! do |a|
|
401
|
-
|
402
|
-
|
500
|
+
if a.is_a?(Symbol)
|
501
|
+
a = (a.to_s.size == 1) ? "-#{a}" : a.to_s
|
502
|
+
end
|
403
503
|
a
|
404
504
|
end
|
405
505
|
[cmd, args]
|
@@ -408,9 +508,11 @@ module Rye
|
|
408
508
|
# Executes +command+ via SSH
|
409
509
|
# Returns an Array with 4 elements: [stdout, stderr, exit code, exit signal]
|
410
510
|
def net_ssh_exec!(command)
|
511
|
+
|
411
512
|
block ||= Proc.new do |channel, type, data|
|
412
513
|
channel[:stdout] ||= ""
|
413
514
|
channel[:stderr] ||= ""
|
515
|
+
channel[:exit_code] ||= -1
|
414
516
|
channel[:stdout] << data if type == :stdout
|
415
517
|
channel[:stderr] << data if type == :stderr
|
416
518
|
channel.on_request("exit-status") do |ch, data|
|
@@ -426,14 +528,19 @@ module Rye
|
|
426
528
|
# command.
|
427
529
|
#channel.on_data do |ch, data|
|
428
530
|
# puts "got stdout: #{data}"
|
429
|
-
# channel.send_data "something for stdin\n"
|
531
|
+
# #channel.send_data "something for stdin\n"
|
532
|
+
#end
|
533
|
+
#
|
534
|
+
#channel.on_extended_data do |ch, data|
|
535
|
+
# #puts "got stdout: #{data}"
|
536
|
+
# #channel.send_data "something for stdin\n"
|
430
537
|
#end
|
431
538
|
end
|
432
539
|
|
433
540
|
channel = @ssh.exec(command, &block)
|
434
541
|
channel.wait # block until we get a response
|
435
542
|
|
436
|
-
channel[:exit_code]
|
543
|
+
channel[:exit_code] = 0 if channel[:exit_code] == nil
|
437
544
|
channel[:exit_code] &&= channel[:exit_code].to_i
|
438
545
|
|
439
546
|
channel[:stderr].gsub!(/bash: line \d+:\s+/, '') if channel[:stderr]
|
@@ -442,6 +549,81 @@ module Rye
|
|
442
549
|
end
|
443
550
|
|
444
551
|
|
552
|
+
# * +direction+ is one of :upload, :download
|
553
|
+
# * +files+ is an Array of file paths, the content is direction specific.
|
554
|
+
# For downloads, +files+ is a list of files to download. The last element
|
555
|
+
# must be the local directory to download to. If downloading a single file
|
556
|
+
# the last element can be a file path. The target can also be a StringIO.
|
557
|
+
# For uploads, +files+ is a list of files to upload. The last element is
|
558
|
+
# the directory to upload to. If uploading a single file, the last element
|
559
|
+
# can be a file path. The list of files can also include StringIO objects.
|
560
|
+
# For both uploads and downloads, the target directory will be created if
|
561
|
+
# it does not exist, but only when multiple files are being transferred.
|
562
|
+
# This method will fail early if there are obvious problems with the input
|
563
|
+
# parameters. An exception is raised and no files are transferred.
|
564
|
+
# Uploads always return nil. Downloads return nil or a StringIO object if
|
565
|
+
# one is specified for the target.
|
566
|
+
def net_scp_transfer!(direction, *files)
|
567
|
+
direction ||= ''
|
568
|
+
unless [:upload, :download].member?(direction.to_sym)
|
569
|
+
raise "Must be one of: upload, download"
|
570
|
+
end
|
571
|
+
|
572
|
+
if @current_working_directory
|
573
|
+
info "CWD (#{@current_working_directory}) not used"
|
574
|
+
end
|
575
|
+
|
576
|
+
files = [files].flatten.compact || []
|
577
|
+
|
578
|
+
# We allow a single file to be downloaded into a StringIO object
|
579
|
+
# but only when no target has been specified.
|
580
|
+
if direction == :download && files.size == 1
|
581
|
+
debug "Created StringIO for download"
|
582
|
+
other = StringIO.new
|
583
|
+
else
|
584
|
+
other = files.pop
|
585
|
+
end
|
586
|
+
|
587
|
+
if direction == :upload && other.is_a?(StringIO)
|
588
|
+
raise "Cannot upload to a StringIO object"
|
589
|
+
end
|
590
|
+
|
591
|
+
# Fail early. We check the
|
592
|
+
files.each do |file|
|
593
|
+
if file.is_a?(StringIO)
|
594
|
+
raise "Cannot download a StringIO object" if direction == :download
|
595
|
+
raise "StringIO object not opened for reading" if file.closed_read?
|
596
|
+
# If a StringIO object is at end of file, SCP will hang. (TODO: SCP)
|
597
|
+
file.rewind if file.eof?
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
debug "#{direction.to_s.upcase} TO: #{other}"
|
602
|
+
debug "FILES: " << files.join(', ')
|
603
|
+
|
604
|
+
# Make sure the remote directory exists. We can do this only when
|
605
|
+
# there's more than one file because "other" could be a file name
|
606
|
+
if files.size > 1 && !other.is_a?(StringIO)
|
607
|
+
debug "CREATING TARGET DIRECTORY: #{other}"
|
608
|
+
self.mkdir(:p, other) unless self.file_exists?(other)
|
609
|
+
end
|
610
|
+
|
611
|
+
Net::SCP.start(@host, @opts[:user], @opts || {}) do |scp|
|
612
|
+
transfers = []
|
613
|
+
files.each do |file|
|
614
|
+
debug file.to_s
|
615
|
+
transfers << scp.send(direction, file, other) do |ch, n, s, t|
|
616
|
+
pinfo "#{n}: #{s}/#{t}b\r" # update line: "file: sent/total"
|
617
|
+
@info.flush if @info # make sure every line is printed
|
618
|
+
end
|
619
|
+
end
|
620
|
+
transfers.each { |t| t.wait } # Run file transfers in parallel
|
621
|
+
info $/
|
622
|
+
end
|
623
|
+
|
624
|
+
other.is_a?(StringIO) ? other : nil
|
625
|
+
end
|
626
|
+
|
445
627
|
|
446
628
|
end
|
447
629
|
end
|
data/lib/rye/cmd.rb
CHANGED
@@ -18,6 +18,12 @@ module Rye;
|
|
18
18
|
# rbox.special # => "special on somehost"
|
19
19
|
#
|
20
20
|
module Cmd
|
21
|
+
|
22
|
+
#--
|
23
|
+
# TODO: Clean this trite mess up!
|
24
|
+
#++
|
25
|
+
|
26
|
+
def cd(*args); cmd('cd', args); end
|
21
27
|
def wc(*args); cmd('wc', args); end
|
22
28
|
def cp(*args); cmd("cp", args); end
|
23
29
|
def mv(*args); cmd("mv", args); end
|
@@ -28,8 +34,8 @@ module Rye;
|
|
28
34
|
def df(*args); cmd('df', args); end
|
29
35
|
def du(*args); cmd('du', args); end
|
30
36
|
|
31
|
-
def env
|
32
|
-
def pwd(*args); cmd "pwd"; end
|
37
|
+
def env; cmd "env"; end
|
38
|
+
def pwd(*args); cmd "pwd", args; end
|
33
39
|
def svn(*args); cmd('svn', args); end
|
34
40
|
def cvs(*args); cmd('cvs', args); end
|
35
41
|
def git(*args); cmd('git', args); end
|
@@ -49,40 +55,100 @@ module Rye;
|
|
49
55
|
def mkfs(*args); cmd('mkfs', args); end
|
50
56
|
|
51
57
|
def mount(*args); cmd("mount", args); end
|
52
|
-
def sleep(
|
58
|
+
def sleep(*args); cmd("sleep", args); end
|
53
59
|
def mkdir(*args); cmd('mkdir', args); end
|
54
60
|
def touch(*args); cmd('touch', args); end
|
55
61
|
def uname(*args); cmd('uname', args); end
|
56
62
|
def chmod(*args); cmd('chmod', args); end
|
63
|
+
def chown(*args); cmd('chown', args); end
|
57
64
|
|
58
65
|
def umount(*args); cmd("umount", args); end
|
59
66
|
def uptime(*args); cmd("uptime", args); end
|
60
67
|
def python(*args); cmd('python', args); end
|
68
|
+
def useradd(*args); cmd('useradd', args); end
|
69
|
+
def getconf(*args); cmd('getconf', args); end
|
61
70
|
def history(*args); cmd('history', args); end
|
62
71
|
def printenv(*args); cmd('printenv', args); end
|
63
72
|
def hostname(*args); cmd('hostname', args); end
|
64
73
|
|
65
|
-
|
66
|
-
#
|
67
|
-
#
|
74
|
+
# Transfer files to a machine via Net::SCP.
|
75
|
+
# * +files+ is an Array of files to upload. The last element is the
|
76
|
+
# directory to upload to. If uploading a single file, the last element
|
77
|
+
# can be a file path. The list of files can also include StringIO objects.
|
78
|
+
# The target directory will be created if it does not exist, but only
|
79
|
+
# when multiple files are being transferred.
|
80
|
+
# This method will fail early if there are obvious problems with the input
|
81
|
+
# parameters. An exception is raised and no files are transferred.
|
82
|
+
# Always return nil.
|
68
83
|
#
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
84
|
+
# NOTE: Changes to current working directory with +cd+ or +[]+ are ignored.
|
85
|
+
def upload(*files); net_scp_transfer!(:upload, *files); end
|
86
|
+
|
87
|
+
# Transfer files from a machine via Net::SCP.
|
88
|
+
# * +files+ is an Array of files to download. The last element must be the
|
89
|
+
# local directory to download to. If downloading a single file the last
|
90
|
+
# element can be a file path. The target can also be a StringIO object.
|
91
|
+
# The target directory will be created if it does not exist, but only
|
92
|
+
# when multiple files are being transferred.
|
93
|
+
# This method will fail early if there are obvious problems with the input
|
94
|
+
# parameters. An exception is raised and no files are transferred.
|
95
|
+
# Return nil or a StringIO object, if specified as the target.
|
96
|
+
#
|
97
|
+
# NOTE: Changes to current working directory with +cd+ or +[]+ are ignored.
|
98
|
+
def download(*files); net_scp_transfer!(:download, *files); end
|
99
|
+
|
100
|
+
# Does a remote path exist?
|
101
|
+
def file_exists?(path)
|
102
|
+
begin
|
103
|
+
ret = self.ls(path)
|
104
|
+
rescue Rye::CommandError => ex
|
105
|
+
ret = ex.rap
|
106
|
+
end
|
107
|
+
# "ls" returns a 0 exit code regardless of success in Linux
|
108
|
+
# But on OSX exit code is 1. This is why we look at STDERR.
|
109
|
+
ret.stderr.empty?
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns the hash containing the parsed output of "env" on the
|
113
|
+
# remote machine. If the initialize option +:getenv+ was set to
|
114
|
+
# false, this will return an empty hash.
|
115
|
+
# This is a lazy loaded method so it fetches the remote envvars
|
116
|
+
# the first time this method is called.
|
117
|
+
#
|
118
|
+
# puts rbox.getenv['HOME'] # => "/home/gloria" (remote)
|
119
|
+
#
|
120
|
+
def getenv
|
121
|
+
if @getenv && @getenv.empty? && self.can?(:env)
|
122
|
+
env = self.env rescue []
|
123
|
+
env.each do |nv|
|
124
|
+
# Parse "GLORIA_HOME=/gloria/lives/here" into a name/value
|
125
|
+
# pair. The regexp ensures we split only at the 1st = sign
|
126
|
+
n, v = nv.scan(/\A([\w_-]+?)=(.+)\z/).flatten
|
127
|
+
@getenv[n] = v
|
128
|
+
end
|
129
|
+
end
|
130
|
+
@getenv
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns an Array of system commands available over SSH
|
134
|
+
def can
|
135
|
+
Rye::Cmd.instance_methods
|
136
|
+
end
|
137
|
+
alias :commands :can
|
138
|
+
alias :cmds :can
|
80
139
|
|
81
|
-
def
|
82
|
-
|
140
|
+
def can?(meth)
|
141
|
+
self.can.member?(RUBY_VERSION =~ /1.9/ ? meth.to_sym : meth.to_s)
|
83
142
|
end
|
143
|
+
alias :command? :can?
|
144
|
+
alias :cmd? :can?
|
145
|
+
|
146
|
+
|
84
147
|
|
85
|
-
|
148
|
+
#--
|
149
|
+
# * Consider a lock-down mode using method_added
|
150
|
+
# * Consider Rye.sysinfo.os == :unix
|
151
|
+
#++
|
86
152
|
end
|
87
153
|
|
88
154
|
end
|
data/lib/rye/rap.rb
CHANGED
@@ -20,7 +20,9 @@ module Rye;
|
|
20
20
|
|
21
21
|
# An array containing any STDERR output
|
22
22
|
attr_reader :stderr
|
23
|
-
|
23
|
+
attr_reader :exit_code
|
24
|
+
# Only populated when calling via Rye::Shell
|
25
|
+
attr_reader :pid
|
24
26
|
attr_accessor :exit_signal
|
25
27
|
|
26
28
|
# The command that was executed.
|
@@ -30,6 +32,8 @@ module Rye;
|
|
30
32
|
# * +args+ anything that can sent to Array#new
|
31
33
|
def initialize(obj, *args)
|
32
34
|
@obj = obj
|
35
|
+
@exit_code = 0
|
36
|
+
@stderr = []
|
33
37
|
super *args
|
34
38
|
end
|
35
39
|
|
@@ -65,12 +69,29 @@ module Rye;
|
|
65
69
|
self.flatten!
|
66
70
|
end
|
67
71
|
|
72
|
+
# Parse the exit code.
|
73
|
+
# * +code+ an exit code string or integer or Process::Status object
|
74
|
+
# For example, when running a command via Rye.shell, this method
|
75
|
+
# is send $? which is Process::Status object. Via Rye::Box.run_command
|
76
|
+
# it's just an exit code returned by Net::SSH.
|
77
|
+
# Returns the exit code as an Integer.
|
78
|
+
def add_exit_code(code)
|
79
|
+
code = -1 unless code
|
80
|
+
if code.is_a?(Process::Status)
|
81
|
+
@exit_code, @pid = code.exitstatus.to_i, code.pid
|
82
|
+
else
|
83
|
+
@exit_code = code.to_i
|
84
|
+
end
|
85
|
+
@exit_code
|
86
|
+
end
|
87
|
+
def code; @exit_code; end
|
88
|
+
|
68
89
|
# Returns the first element if there it's the only
|
69
90
|
# one, otherwise the value of Array#to_s
|
70
91
|
def to_s
|
71
92
|
return self.first if self.size == 1
|
72
93
|
return "" if self.size == 0
|
73
|
-
|
94
|
+
self
|
74
95
|
end
|
75
96
|
|
76
97
|
# NOTE: This is broken!
|
data/lib/rye/set.rb
CHANGED
@@ -68,11 +68,11 @@ module Rye
|
|
68
68
|
|
69
69
|
# Add an environment variable. +n+ and +v+ are the name and value.
|
70
70
|
# Returns the instance of Rye::Set
|
71
|
-
def
|
72
|
-
run_command(:
|
71
|
+
def setenv(n, v)
|
72
|
+
run_command(:setenv, n, v)
|
73
73
|
self
|
74
74
|
end
|
75
|
-
alias :
|
75
|
+
alias :setenvironment_variable :setenv
|
76
76
|
|
77
77
|
# See Rye.keys
|
78
78
|
def keys
|
data/lib/sys.rb
CHANGED
@@ -274,6 +274,19 @@ class SystemInfo #:nodoc:all
|
|
274
274
|
end
|
275
275
|
end
|
276
276
|
|
277
|
+
def tmpdir
|
278
|
+
if @os == :unix
|
279
|
+
(ENV['TMPDIR'] || '/tmp')
|
280
|
+
elsif @os == :win32
|
281
|
+
(ENV['TMPDIR'] || 'C:\\temp')
|
282
|
+
elsif @os == :java
|
283
|
+
default = @impl == :windows ? 'C:\\temp' : '/tmp'
|
284
|
+
(ENV['TMPDIR'] || default)
|
285
|
+
else
|
286
|
+
raise "paths not implemented for: #{@os}"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
277
290
|
# Print friendly system information.
|
278
291
|
def to_s
|
279
292
|
sprintf("Hostname: %s#{$/}IP Address: %s#{$/}System: %s#{$/}Uptime: %.2f (hours)#{$/}Ruby: #{ruby.join('.')}",
|
data/rye.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
@spec = Gem::Specification.new do |s|
|
2
2
|
s.name = "rye"
|
3
3
|
s.rubyforge_project = "rye"
|
4
|
-
s.version = "0.
|
4
|
+
s.version = "0.6.0"
|
5
5
|
s.summary = "Rye: Safely run SSH commands on a bunch of machines at the same time (from Ruby)."
|
6
6
|
s.description = s.summary
|
7
7
|
s.author = "Delano Mandelbaum"
|
@@ -15,6 +15,11 @@
|
|
15
15
|
s.add_dependency 'highline'
|
16
16
|
s.add_dependency 'drydock'
|
17
17
|
|
18
|
+
# = EXECUTABLES =
|
19
|
+
# The list of executables in your project (if any). Don't include the path,
|
20
|
+
# just the base filename.
|
21
|
+
s.executables = %w[rye]
|
22
|
+
|
18
23
|
# = MANIFEST =
|
19
24
|
# The complete list of files to be included in the release. When GitHub packages your gem,
|
20
25
|
# it doesn't allow you to run any command that accesses the filesystem. You will get an
|
@@ -44,15 +49,12 @@
|
|
44
49
|
tst/10-key2
|
45
50
|
tst/10-key2.pub
|
46
51
|
tst/10_keys_test.rb
|
47
|
-
tst/
|
52
|
+
tst/50_rset_test.rb
|
53
|
+
tst/60-file.mp3
|
54
|
+
tst/60_rbox_transfer_test.rb
|
55
|
+
tst/70_rbox_env_test.rb
|
48
56
|
)
|
49
57
|
|
50
|
-
# = EXECUTABLES =
|
51
|
-
# The list of executables in your project (if any). Don't include the path,
|
52
|
-
# just the base filename.
|
53
|
-
s.executables = %w[rye]
|
54
|
-
|
55
|
-
|
56
58
|
s.extra_rdoc_files = %w[README.rdoc LICENSE.txt]
|
57
59
|
s.has_rdoc = true
|
58
60
|
s.rdoc_options = ["--line-numbers", "--title", s.summary, "--main", "README.rdoc"]
|
data/tst/60-file.mp3
ADDED
Binary file
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# Usage: test/60_rbox_upload_test.rb
|
5
|
+
#
|
6
|
+
|
7
|
+
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
8
|
+
|
9
|
+
require "rubygems"
|
10
|
+
require "stringio"
|
11
|
+
require "yaml"
|
12
|
+
require "rye"
|
13
|
+
|
14
|
+
tmpdir = Rye.sysinfo.tmpdir
|
15
|
+
|
16
|
+
rbox = Rye::Box.new("localhost", :info => true)
|
17
|
+
def rbox.rm(*args); cmd('rm', args); end
|
18
|
+
rbox.rm(:r, :f, "#{tmpdir}/rye-upload") # Silently delete test dir
|
19
|
+
|
20
|
+
# /tmp/rye-upload will be created if it doesn't exist
|
21
|
+
rbox.upload("README.rdoc", "LICENSE.txt", "#{tmpdir}/rye-upload")
|
22
|
+
|
23
|
+
# A single file can be renamed
|
24
|
+
rbox.upload("README.rdoc", "#{tmpdir}/rye-upload/README-renamed")
|
25
|
+
|
26
|
+
# StringIO objects can be sent as files
|
27
|
+
applejack = StringIO.new
|
28
|
+
applejack.puts "Delano: What's happening Applejack?"
|
29
|
+
applejack.puts "Applejack: Just trying to get by."
|
30
|
+
rbox.upload(applejack, "#{tmpdir}/rye-upload/applejack")
|
31
|
+
|
32
|
+
rbox.upload("tst/60-file.mp3", "#{tmpdir}/rye-upload") # demonstrates
|
33
|
+
rbox.upload("tst/60-file.mp3", "#{tmpdir}/rye-upload") # progress
|
34
|
+
rbox.upload("tst/60-file.mp3", "#{tmpdir}/rye-upload") # bar
|
35
|
+
|
36
|
+
puts "Content of /tmp/rye-upload"
|
37
|
+
puts rbox.ls(:l, "#{tmpdir}/rye-upload")
|
38
|
+
|
39
|
+
rbox.download("#{tmpdir}/rye-upload/README.rdoc",
|
40
|
+
"#{tmpdir}/rye-upload/README-renamed", "#{tmpdir}/rye-download")
|
41
|
+
|
42
|
+
# You can't download a StringIO object. This raises an exception:
|
43
|
+
#rbox.download(applejack, "#{tmpdir}/rye-download/applejack")
|
44
|
+
# But you can download _to_ a StringIO object
|
45
|
+
applejack = StringIO.new
|
46
|
+
rbox.download("#{tmpdir}/rye-upload/applejack", applejack)
|
47
|
+
|
48
|
+
puts $/, "Content of /tmp/rye-download"
|
49
|
+
puts rbox.ls(:l, "#{tmpdir}/rye-download")
|
50
|
+
|
51
|
+
puts $/, "Content of applejack StringIO object"
|
52
|
+
applejack.rewind
|
53
|
+
puts applejack.read
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# Usage: test/70_rbox_env_test.rb
|
5
|
+
#
|
6
|
+
|
7
|
+
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
8
|
+
|
9
|
+
require "rubygems"
|
10
|
+
require "stringio"
|
11
|
+
require "yaml"
|
12
|
+
require "rye"
|
13
|
+
|
14
|
+
tmpdir = Rye.sysinfo.tmpdir
|
15
|
+
|
16
|
+
rbox = Rye::Box.new("localhost", :info => true)
|
17
|
+
|
18
|
+
puts rbox.getenv['HOME'] # slight delay while vars are fetched
|
19
|
+
puts rbox.getenv['HOME'] # returned from memory
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rye
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-04-
|
12
|
+
date: 2009-04-28 00:00:00 -04:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -84,7 +84,10 @@ files:
|
|
84
84
|
- tst/10-key2
|
85
85
|
- tst/10-key2.pub
|
86
86
|
- tst/10_keys_test.rb
|
87
|
-
- tst/
|
87
|
+
- tst/50_rset_test.rb
|
88
|
+
- tst/60-file.mp3
|
89
|
+
- tst/60_rbox_transfer_test.rb
|
90
|
+
- tst/70_rbox_env_test.rb
|
88
91
|
has_rdoc: true
|
89
92
|
homepage: http://solutious.com/
|
90
93
|
licenses: []
|