rye 0.3.2 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
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