rye 0.3.2 → 0.4

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/lib/rye/box.rb CHANGED
@@ -33,7 +33,9 @@ module Rye
33
33
  attr_accessor :safe
34
34
  attr_accessor :opts
35
35
 
36
-
36
+ # The most recent value from Box.cd or Box.[]
37
+ attr_reader :current_working_directory
38
+
37
39
  # * +host+ The hostname to connect to. The default is localhost.
38
40
  # * +opts+ a hash of optional arguments.
39
41
  #
@@ -92,16 +94,17 @@ module Rye
92
94
  alias :commands :can
93
95
  alias :cmds :can
94
96
 
95
-
97
+
98
+
96
99
  # Change the current working directory (sort of).
97
100
  #
98
101
  # I haven't been able to wrangle Net::SSH to do my bidding.
99
102
  # "My bidding" in this case, is maintaining an open channel between commands.
100
- # I'm using Net::SSH::Connection::Session#exec! for all commands
103
+ # I'm using Net::SSH::Connection::Session#exec for all commands
101
104
  # which is like a funky helper method that opens a new channel
102
105
  # each time it's called. This seems to be okay for one-off
103
106
  # commands but changing the directory only works for the channel
104
- # it's executed in. The next time exec! is called, there's a
107
+ # it's executed in. The next time exec is called, there's a
105
108
  # new channel which is back in the default (home) directory.
106
109
  #
107
110
  # Long story short, the work around is to maintain the current
@@ -117,7 +120,6 @@ module Rye
117
120
  end
118
121
  alias :cd :'[]'
119
122
 
120
-
121
123
  # Open an SSH session with +@host+. This called automatically
122
124
  # when you the first comamnd is run if it's not already connected.
123
125
  # Raises a Rye::NoHost exception if +@host+ is not specified.
@@ -163,148 +165,162 @@ module Rye
163
165
  Rye.keys
164
166
  end
165
167
 
168
+ # Returns +@host+
169
+ def to_s
170
+ @host
171
+ end
166
172
 
167
- # Takes a command with arguments and returns it in a
168
- # single String with escaped args and some other stuff.
169
- #
170
- # * +cmd+ The shell command name or absolute path.
171
- # * +args+ an Array of command arguments.
172
- #
173
- # The command is searched for in the local PATH (where
174
- # Rye is running). An exception is raised if it's not
175
- # found. NOTE: Because this happens locally, you won't
176
- # want to use this method if the environment is quite
177
- # different from the remote machine it will be executed
178
- # on.
179
- #
180
- # The command arguments are passed through Escape.shell_command
181
- # (that means you can't use environment variables or asterisks).
182
- #
183
- def Box.prepare_command(cmd, *args)
184
- args &&= [args].flatten.compact
185
- cmd = Rye::Box.which(cmd)
186
- raise CommandNotFound.new(cmd || 'nil') unless cmd
187
- Rye::Box.escape(@safe, cmd, *args)
173
+ def inspect
174
+ %q{#<%s:%s cwd=%s env=%s safe=%s opts=%s>} %
175
+ [self.class.to_s, self.host,
176
+ @current_working_directory, (@current_environment_variables || '').inspect,
177
+ self.safe, self.opts.inspect]
188
178
  end
189
179
 
190
- # An all ruby implementation of unix "which" command.
191
- #
192
- # * +executable+ the name of the executable
193
- #
194
- # Returns the absolute path if found in PATH otherwise nil.
195
- def Box.which(executable)
196
- return unless executable.is_a?(String)
197
- #return executable if File.exists?(executable) # SHOULD WORK, MUST TEST
198
- shortname = File.basename(executable)
199
- dir = Rye.sysinfo.paths.select do |path| # dir contains all of the
200
- next unless File.exists? path # occurrences of shortname
201
- Dir.new(path).entries.member?(shortname) # found in the paths.
202
- end
203
- File.join(dir.first, shortname) unless dir.empty? # Return just the first
180
+ # Compares itself with the +other+ box. If the hostnames
181
+ # are the same, this will return true. Otherwise false.
182
+ def ==(other)
183
+ @host == other.host
184
+ end
185
+
186
+ # Returns the host SSH keys for this box
187
+ def host_key
188
+ raise "No host" unless @host
189
+ Rye.remote_host_keys(@host)
204
190
  end
205
191
 
206
- # Execute a local system command (via the shell, not SSH)
207
- #
208
- # * +cmd+ the executable path (relative or absolute)
209
- # * +args+ Array of arguments to be sent to the command. Each element
210
- # is one argument:. i.e. <tt>['-l', 'some/path']</tt>
192
+ # Copy the local public keys (as specified by Rye.keys) to
193
+ # this box into ~/.ssh/authorized_keys and ~/.ssh/authorized_keys2.
194
+ # Returns an Array of the private keys files used to generate the public keys.
211
195
  #
212
- # NOTE: shell is a bit paranoid so it escapes every argument. This means
213
- # you can only use literal values. That means no asterisks too.
196
+ # NOTE: authorize_keys disables safe-mode for this box while it runs.
214
197
  #
215
- def Box.shell(cmd, args=[])
216
- # TODO: allow stdin to be send to cmd
217
- cmd = Box.prepare_command(cmd, args)
218
- cmd << " 2>&1" # Redirect STDERR to STDOUT. Works in DOS also.
219
- handle = IO.popen(cmd, "r")
220
- output = handle.read.chomp
221
- handle.close
222
- output
198
+ def authorize_keys
199
+ added_keys = []
200
+ opts[:safe] = false
201
+ Rye.keys.each do |key|
202
+ path = key[2]
203
+ debug "# Public key for #{path}"
204
+ k = Rye::Key.from_file(path).public_key.to_ssh2
205
+ self.mkdir('-p', '~/.ssh') # Silently create dir if it doesn't exist
206
+ self.echo("'#{k}' >> ~/.ssh/authorized_keys")
207
+ self.echo("'#{k}' >> ~/.ssh/authorized_keys2")
208
+ self.chmod('-R', '0600', '.ssh')
209
+ added_keys << path
210
+ end
211
+ opts[:safe] = true
212
+ added_keys
223
213
  end
224
214
 
225
- # Creates a string from +cmd+ and +args+. If +safe+ is true
226
- # it will send them through Escape.shell_command otherwise
227
- # it will return them joined by a space character.
228
- def Box.escape(safe, cmd, *args)
229
- args = args.flatten.compact || []
230
- safe ? Escape.shell_command(cmd, *args).to_s : [cmd, args].flatten.compact.join(' ')
215
+ private
216
+
217
+
218
+ def debug(msg); @debug.puts msg if @debug; end
219
+ def error(msg); @error.puts msg if @error; end
220
+
221
+
222
+ # Add the current environment variables to the beginning of +cmd+
223
+ def prepend_env(cmd)
224
+ return cmd unless @current_environment_variables.is_a?(Hash)
225
+ env = ''
226
+ @current_environment_variables.each_pair do |n,v|
227
+ env << "export #{n}=#{Escape.shell_single_word(v)}; "
228
+ end
229
+ [env, cmd].join(' ')
231
230
  end
232
231
 
233
- private
234
-
235
-
236
- def debug(msg); @debug.puts msg if @debug; end
237
- def error(msg); @error.puts msg if @error; end
238
232
 
233
+ # Execute a command over SSH
234
+ #
235
+ # * +args+ is a command name and list of arguments.
236
+ # The command name is the literal name of the command
237
+ # that will be executed in the remote shell. The arguments
238
+ # will be thoroughly escaped and passed to the command.
239
+ #
240
+ # rbox = Rye::Box.new
241
+ # rbox.ls '-l', 'arg1', 'arg2'
242
+ #
243
+ # is equivalent to
244
+ #
245
+ # $ ls -l 'arg1' 'arg2'
246
+ #
247
+ # This method will try to connect to the host automatically
248
+ # but if it fails it will raise a Rye::NotConnected exception.
249
+ #
250
+ def run_command(*args)
251
+ connect if !@ssh || @ssh.closed?
252
+ args = args.flatten.compact
253
+ args = args.first.split(/\s+/) if args.size == 1
254
+ cmd = args.shift
239
255
 
240
- # Add the current environment variables to the beginning of +cmd+
241
- def prepend_env(cmd)
242
- return cmd unless @current_environment_variables.is_a?(Hash)
243
- env = ''
244
- @current_environment_variables.each_pair do |n,v|
245
- env << "export #{n}=#{Escape.shell_single_word(v)}; "
246
- end
247
- [env, cmd].join(' ')
256
+ # Symbols to switches. :l -> -l, :help -> --help
257
+ args.collect! do |a|
258
+ a = "-#{a}" if a.is_a?(Symbol) && a.to_s.size == 1
259
+ a = "--#{a}" if a.is_a?(Symbol)
260
+ a
248
261
  end
249
262
 
263
+ raise Rye::NotConnected, @host unless @ssh && !@ssh.closed?
250
264
 
251
- # Execute a command over SSH
252
- #
253
- # * +args+ is a command name and list of arguments.
254
- # The command name is the literal name of the command
255
- # that will be executed in the remote shell. The arguments
256
- # will be thoroughly escaped and passed to the command.
257
- #
258
- # rbox = Rye::Box.new
259
- # rbox.ls '-l', 'arg1', 'arg2'
260
- #
261
- # is equivalent to
262
- #
263
- # $ ls -l 'arg1' 'arg2'
264
- #
265
- # This method will try to connect to the host automatically
266
- # but if it fails it will raise a Rye::NotConnected exception.
267
- #
268
- def add_command(*args)
269
- connect if !@ssh || @ssh.closed?
270
- args = args.first.split(/\s+/) if args.size == 1
271
- cmd, args = args.flatten.compact
272
-
273
- raise Rye::NotConnected, @host unless @ssh && !@ssh.closed?
274
-
275
- cmd_clean = Rye::Box.escape(@safe, cmd, args)
276
- cmd_clean = prepend_env(cmd_clean)
277
- if @current_working_directory
278
- cwd = Rye::Box.escape(@safe, 'cd', @current_working_directory)
279
- cmd_clean = [cwd, cmd_clean].join('; ')
280
- end
281
- debug "Executing: %s" % cmd_clean
282
- stdout, stderr = net_ssh_exec! cmd_clean
283
- rap = Rye::Rap.new(self)
284
- rap.add_stdout(stdout || '')
285
- rap.add_stderr(stderr || '')
286
- rap
265
+ cmd_clean = Rye.escape(@safe, cmd, args)
266
+ cmd_clean = prepend_env(cmd_clean)
267
+ if @current_working_directory
268
+ cwd = Rye.escape(@safe, 'cd', @current_working_directory)
269
+ cmd_clean = [cwd, cmd_clean].join(' && ')
287
270
  end
288
- alias :cmd :add_command
289
271
 
290
- # Executes +command+ via SSH
291
- # Returns an Array with two elements, [stdout, stderr], representing
292
- # the STDOUT and STDERR output by the command. They're Strings.
293
- def net_ssh_exec!(command)
294
- block ||= Proc.new do |ch, type, data|
295
- ch[:stdout] ||= ""
296
- ch[:stderr] ||= ""
297
- ch[:stdout] << data if type == :stdout
298
- ch[:stderr] << data if type == :stderr
272
+ debug "Executing: %s" % cmd_clean
273
+ stdout, stderr, ecode, esignal = net_ssh_exec! cmd_clean
274
+ rap = Rye::Rap.new(self)
275
+ rap.add_stdout(stdout || '')
276
+ rap.add_stderr(stderr || '')
277
+ rap.exit_code = ecode
278
+ rap.exit_signal = esignal
279
+ rap.cmd = cmd
280
+
281
+ raise Rye::CommandError.new(rap) if ecode > 0
282
+
283
+ rap
284
+ end
285
+ alias :cmd :run_command
286
+
287
+ # Executes +command+ via SSH
288
+ # Returns an Array with 4 elements: [stdout, stderr, exit code, exit signal]
289
+ def net_ssh_exec!(command)
290
+ block ||= Proc.new do |channel, type, data|
291
+ channel[:stdout] ||= ""
292
+ channel[:stderr] ||= ""
293
+ channel[:stdout] << data if type == :stdout
294
+ channel[:stderr] << data if type == :stderr
295
+ channel.on_request("exit-status") do |ch, data|
296
+ # Anything greater than 0 is an error
297
+ channel[:exit_code] = data.read_long
298
+ end
299
+ channel.on_request("exit-signal") do |ch, data|
300
+ # This should be the POSIX SIGNAL that ended the process
301
+ channel[:exit_signal] = data.read_long
299
302
  end
300
-
301
- channel = @ssh.exec(command, &block)
302
- channel.wait # block until we get a response
303
-
304
- [channel[:stdout], channel[:stderr]]
303
+ # For long-running commands like top, this will print the output.
304
+ # It cool, but we'd also need to enable STDIN to interact with
305
+ # command.
306
+ #channel.on_data do |ch, data|
307
+ # puts "got stdout: #{data}"
308
+ # channel.send_data "something for stdin\n"
309
+ #end
305
310
  end
306
311
 
312
+ channel = @ssh.exec(command, &block)
313
+ channel.wait # block until we get a response
307
314
 
315
+ channel[:exit_code] ||= 0
316
+ channel[:exit_code] &&= channel[:exit_code].to_i
317
+
318
+ channel[:stderr].gsub!(/bash: line \d+:\s+/, '') if channel[:stderr]
319
+
320
+ [channel[:stdout], channel[:stderr], channel[:exit_code], channel[:exit_signal]]
321
+ end
322
+
323
+
308
324
 
309
325
  end
310
326
  end
data/lib/rye/cmd.rb CHANGED
@@ -43,16 +43,39 @@ module Rye;
43
43
  def perl(*args); cmd('perl', args); end
44
44
  def bash(*args); cmd('bash', args); end
45
45
  def echo(*args); cmd('echo', args); end
46
+ def test(*args); cmd('test', args); end
46
47
 
47
48
  def mount; cmd("mount"); end
48
49
  def sleep(seconds=1); cmd("sleep", seconds); end
50
+ def mkdir(*args); cmd('mkdir', args); end
49
51
  def touch(*args); cmd('touch', args); end
50
52
  def uname(*args); cmd('uname', args); end
53
+ def chmod(*args); cmd('uname', args); end
51
54
 
52
55
  def uptime; cmd("uptime"); end
53
56
  def python(*args); cmd('python', args); end
54
57
  def printenv(*args); cmd('printenv', args); end
55
58
 
59
+
60
+ # def copy_to(*boxes)
61
+ # p boxes
62
+ #
63
+ # @scp = Net::SCP.start(@host, @opts[:user], @opts || {})
64
+ # #@ssh.is_a?(Net::SSH::Connection::Session) && !@ssh.closed?
65
+ # p @scp
66
+ # end
67
+
68
+
69
+ #def copy_to(*args)
70
+ # args = [args].flatten.compact || []
71
+ # other = args.pop
72
+ # p other
73
+ #end
74
+
75
+ def exists?
76
+ cmd("uptime");
77
+ end
78
+
56
79
  # Consider Rye.sysinfo.os == :unix
57
80
  end
58
81
 
data/lib/rye/key.rb ADDED
@@ -0,0 +1,134 @@
1
+
2
+ module Rye
3
+ class Key
4
+ class BadFile < RuntimeError
5
+ def initialize(m); @m = m; end
6
+ def message; "That ain't a no key. #{$/}#{@m}"; end
7
+ end
8
+ class BadPerm < RuntimeError
9
+ def initialize(m); @m = m; end
10
+ def message; "Bad file permissions. Set to 0600. #{$/}#{@m}"; end
11
+ end
12
+
13
+ # A nickname for this key. If a path was specified this defaults to the basename.
14
+ attr_reader :name
15
+ # Authentication type: RSA or DSA
16
+ attr_reader :authtype
17
+ # Key type: public or private
18
+ attr_reader :keytype
19
+
20
+ def initialize(data, name=nil)
21
+ @data = data
22
+ @name = name || 'default'
23
+ parse_data
24
+ end
25
+
26
+ def self.generate_pkey(authtype="RSA", bits=1024)
27
+ unless Rye::Key.supported_authentication?(authtype)
28
+ raise OpenSSL::PKey::PKeyError, "Unknown authentication: #{authttype}"
29
+ end
30
+ bits &&= bits.to_i
31
+ klass = authtype.upcase == "RSA" ? OpenSSL::PKey::RSA : OpenSSL::PKey::DSA
32
+ pk = klass.new(bits)
33
+ end
34
+
35
+ def self.from_file(path)
36
+ raise BadFile, path unless File.exists?(path || '')
37
+ pkey = self.new File.read(path), File.basename(path)
38
+ file_perms = (File.stat(path).mode & 600)
39
+ raise BadPerm, path if file_perms != 0 && pkey.private?
40
+ pkey
41
+ end
42
+
43
+
44
+ def sign(string, digesttype="sha1")
45
+ Rye::Key.sign(@keypair.to_s, string, digesttype)
46
+ end
47
+
48
+ def self.sign(secret, string, digesttype="sha1")
49
+ @@digest ||= {}
50
+ @@digest[digest] ||= OpenSSL::Digest::Digest.new(digesttype)
51
+ sig = OpenSSL::HMAC.hexdigest(@@digest[digest], secret, string).strip
52
+ end
53
+ def self.sign_aws(secret, string)
54
+ ::Base64.encode64(self.sign(secret, string, "sha1")).strip
55
+ end
56
+
57
+ def private_key
58
+ raise OpenSSL::PKey::PKeyError, "No private key" if public? || !@keypair
59
+ @keypair.to_s
60
+ end
61
+
62
+ def public_key
63
+ raise OpenSSL::PKey::PKeyError, "No public key" if !@keypair
64
+ pubkey = public? ? @keypair : @keypair.public_key
65
+ # Add the to_ssh2 method to the instance of OpenSSL::PKey::*SA only
66
+ def pubkey.to_ssh2; Rye::Key.public_key_to_ssh2(self); end
67
+ pubkey
68
+ end
69
+
70
+ # Encrypt +text+ with this public or private key. The key must
71
+ def encrypt(text); ::Base64.encode64(@keypair.send("#{keytype.downcase}_encrypt", text)); end
72
+ def decrypt(text); @keypair.send("#{keytype.downcase}_decrypt", ::Base64.decode64(text)); end
73
+
74
+ def private?; @keytype.upcase == "PRIVATE"; end
75
+ def public?; @keytype.upcase == "PUBLIC"; end
76
+ def rsa?; @authtype.upcase == "RSA"; end
77
+ def dsa?; @authtype.upcase == "DSA"; end
78
+ def encrypted?; @data && @data.match(/ENCRYPTED/); end
79
+
80
+ # * +pubkey+ an instance of OpenSSL::PKey::RSA or OpenSSL::PKey::DSA
81
+ # Returns a public key in SSH format (suitable for ~/.ssh/authorized_keys)
82
+ def self.public_key_to_ssh2(pubkey)
83
+ authtype = pubkey.class.to_s.split('::').last.downcase
84
+ b64pub = ::Base64.encode64(pubkey.to_blob).strip.gsub(/[\r\n]/, '')
85
+ "ssh-%s %s" % [authtype, b64pub] # => ssh-rsa AAAAB3NzaC1...=
86
+ end
87
+
88
+ def dump
89
+ puts @keypair.public_key.to_text
90
+ puts @keypair.public_key.to_pem
91
+ end
92
+
93
+ # Reveals the key basename. Does not print the key.
94
+ #
95
+ # <Rye::Key:id_rsa.pub>
96
+ #
97
+ def to_s
98
+ '<%s:%s>' % [self.class.to_s, name]
99
+ end
100
+
101
+ # Reveals some metadata about the key. Does not print the key.
102
+ #
103
+ # <Rye::Key:id_rsa.pub authtype="RSA" keytype="PRIVATE">
104
+ #
105
+ def inspect
106
+ '<%s:%s authtype="%s" keytype="%s">' % [self.class.to_s, name, @authtype, @keytype]
107
+ end
108
+
109
+ def self.supported_authentication?(val)
110
+ ["RSA", "DSA"].member?(val || '')
111
+ end
112
+
113
+ def self.supported_keytype?(val)
114
+ ["PRIVATE", "PUBLIC"].member?(val || '')
115
+ end
116
+
117
+ private
118
+ # Creates an OpenSSL::PKey object from +@data+.
119
+ def parse_data
120
+ # NOTE: Don't print @data. Not even in debug output. The same goes for +@keypair+.
121
+ # We don't want private keys to end up somewhere we don't expect them.
122
+ raise OpenSSL::PKey::PKeyError, "No key data" if @data.nil?
123
+ @data.strip!
124
+ @data =~ /\A-----BEGIN (\w+?) (P\w+?) KEY-----$/ # \A matches the string beginning (^ works on lines)
125
+ raise OpenSSL::PKey::PKeyError, "Bad key data" unless $1 && $2
126
+ raise OpenSSL::PKey::PKeyError, "Unknown type #{$1}" unless Rye::Key.supported_authentication?($1)
127
+ raise OpenSSL::PKey::PKeyError, "Unknown value #{$2}" unless Rye::Key.supported_keytype?($2)
128
+ @authtype, @keytype = $1, $2
129
+ @keypair = OpenSSL::PKey::RSA.new(@data) if self.rsa?
130
+ @keypair = OpenSSL::PKey::DSA.new(@data) if self.dsa?
131
+ end
132
+
133
+ end
134
+ end