rye 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rye/box.rb ADDED
@@ -0,0 +1,312 @@
1
+
2
+
3
+ module Rye
4
+
5
+
6
+ # = Rye::Box
7
+ #
8
+ # The Rye::Box class represents a machine. All system
9
+ # commands are made through this class.
10
+ #
11
+ # rbox = Rye::Box.new('filibuster')
12
+ # rbox.hostname # => filibuster
13
+ # rbox.uname # => FreeBSD
14
+ # rbox.uptime # => 20:53 up 1 day, 1:52, 4 users
15
+ #
16
+ # You can also run local commands through SSH
17
+ #
18
+ # rbox = Rye::Box.new('localhost')
19
+ # rbox.hostname # => localhost
20
+ # rbox.uname # => Darwin
21
+ #
22
+ class Box
23
+ include Rye::Cmd
24
+
25
+ # An instance of Net::SSH::Connection::Session
26
+ attr_reader :ssh
27
+
28
+ attr_reader :debug
29
+ attr_reader :error
30
+
31
+ attr_accessor :host
32
+
33
+ attr_accessor :safe
34
+ attr_accessor :opts
35
+
36
+
37
+ # * +host+ The hostname to connect to. The default is localhost.
38
+ # * +opts+ a hash of optional arguments.
39
+ #
40
+ # The +opts+ hash excepts the following keys:
41
+ #
42
+ # * :user => the username to connect as. Default: the current user.
43
+ # * :safe => should Rye be safe? Default: true
44
+ # * :keys => one or more private key file paths (passwordless login)
45
+ # * :password => the user's password (ignored if there's a valid private key)
46
+ # * :debug => an IO object to print Rye::Box debugging info to. Default: nil
47
+ # * :error => an IO object to print Rye::Box errors to. Default: STDERR
48
+ #
49
+ # NOTE: +opts+ can also contain any parameter supported by
50
+ # Net::SSH.start that is not already mentioned above.
51
+ #
52
+ def initialize(host='localhost', opts={})
53
+
54
+ # These opts are use by Rye::Box and also passed to Net::SSH
55
+ @opts = {
56
+ :user => Rye.sysinfo.user,
57
+ :safe => true,
58
+ :port => 22,
59
+ :keys => [],
60
+ :debug => nil,
61
+ :error => STDERR,
62
+ }.merge(opts)
63
+
64
+ # See Net::SSH.start
65
+ @opts[:paranoid] = true unless @opts[:safe] == false
66
+
67
+ # Close the SSH session before Ruby exits. This will do nothing
68
+ # if disconnect has already been called explicitly.
69
+ at_exit {
70
+ self.disconnect
71
+ }
72
+
73
+ @host = host
74
+
75
+ @safe = @opts.delete(:safe)
76
+ @debug = @opts.delete(:debug)
77
+ @error = @opts.delete(:error)
78
+
79
+ add_keys(@opts[:keys])
80
+
81
+ # We don't want Net::SSH to handle the keypairs. This may change
82
+ # but for we're letting ssh-agent do it.
83
+ @opts.delete(:keys)
84
+
85
+ end
86
+
87
+
88
+ # Returns an Array of system commands available over SSH
89
+ def can
90
+ Rye::Cmd.instance_methods
91
+ end
92
+ alias :commands :can
93
+ alias :cmds :can
94
+
95
+
96
+ # Change the current working directory (sort of).
97
+ #
98
+ # I haven't been able to wrangle Net::SSH to do my bidding.
99
+ # "My bidding" in this case, is maintaining an open channel between commands.
100
+ # I'm using Net::SSH::Connection::Session#exec! for all commands
101
+ # which is like a funky helper method that opens a new channel
102
+ # each time it's called. This seems to be okay for one-off
103
+ # commands but changing the directory only works for the channel
104
+ # it's executed in. The next time exec! is called, there's a
105
+ # new channel which is back in the default (home) directory.
106
+ #
107
+ # Long story short, the work around is to maintain the current
108
+ # directory locally and send it with each command.
109
+ #
110
+ # rbox.pwd # => /home/rye ($ pwd )
111
+ # rbox['/usr/bin'].pwd # => /usr/bin ($ cd /usr/bin && pwd)
112
+ # rbox.pwd # => /usr/bin ($ cd /usr/bin && pwd)
113
+ #
114
+ def [](key=nil)
115
+ @current_working_directory = key
116
+ self
117
+ end
118
+ alias :cd :'[]'
119
+
120
+
121
+ # Open an SSH session with +@host+. This called automatically
122
+ # when you the first comamnd is run if it's not already connected.
123
+ # Raises a Rye::NoHost exception if +@host+ is not specified.
124
+ def connect
125
+ raise Rye::NoHost unless @host
126
+ disconnect if @ssh
127
+ debug "Opening connection to #{@host}"
128
+ @ssh = Net::SSH.start(@host, @opts[:user], @opts || {})
129
+ @ssh.is_a?(Net::SSH::Connection::Session) && !@ssh.closed?
130
+ self
131
+ end
132
+
133
+ # Close the SSH session with +@host+. This is called
134
+ # automatically at exit if the connection is open.
135
+ def disconnect
136
+ return unless @ssh && !@ssh.closed?
137
+ @ssh.loop(0.1) { @ssh.busy? }
138
+ debug "Closing connection to #{@ssh.host}"
139
+ @ssh.close
140
+ end
141
+
142
+ # Add one or more private keys to the SSH Agent.
143
+ # * +additional_keys+ is a list of file paths to private keys
144
+ # Returns the instance of Box
145
+ def add_keys(*additional_keys)
146
+ additional_keys = [additional_keys].flatten.compact || []
147
+ Rye.add_keys(additional_keys)
148
+ self
149
+ end
150
+ alias :add_key :add_keys
151
+
152
+ # Add an environment variable. +n+ and +v+ are the name and value.
153
+ # Returns the instance of Rye::Box
154
+ def add_env(n, v)
155
+ debug "Added env: #{n}=#{v}"
156
+ (@current_environment_variables ||= {})[n] = v
157
+ self
158
+ end
159
+ alias :add_environment_variable :add_env
160
+
161
+ # See Rye.keys
162
+ def keys
163
+ Rye.keys
164
+ end
165
+
166
+
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)
188
+ end
189
+
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
204
+ end
205
+
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>
211
+ #
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.
214
+ #
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
223
+ end
224
+
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(' ')
231
+ end
232
+
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
+
239
+
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(' ')
248
+ end
249
+
250
+
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
287
+ end
288
+ alias :cmd :add_command
289
+
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
299
+ end
300
+
301
+ channel = @ssh.exec(command, &block)
302
+ channel.wait # block until we get a response
303
+
304
+ [channel[:stdout], channel[:stderr]]
305
+ end
306
+
307
+
308
+
309
+ end
310
+ end
311
+
312
+
data/lib/rye/cmd.rb ADDED
@@ -0,0 +1,47 @@
1
+
2
+ module Rye;
3
+
4
+ # = Rye::Cmd
5
+ #
6
+ # This class contains all of the shell command methods
7
+ # available to an instance of Rye::Box. For security and
8
+ # general safety, Rye only permits this whitelist of
9
+ # commands by default. However, you're free to add methods
10
+ # with mixins.
11
+ #
12
+ # require 'rye'
13
+ # module Rye::Box::Cmd
14
+ # def special(*args); cmd("/your/special/command", args); end
15
+ # end
16
+ #
17
+ # rbox = Rye::Box.new
18
+ # rbox.special # => "your output"
19
+ #
20
+ module Cmd
21
+ def wc(*args); cmd('wc', args); end
22
+ def cp(*args); cmd("cp", args); end
23
+ def mv(*args); cmd("mv", args); end
24
+ def ls(*args); cmd('ls', args); end
25
+ def rm(*args); cmd('rm', args); end
26
+ def ps(*args); cmd('ps', args); end
27
+ def sh(*args); cmd('sh', args); end
28
+ def env; cmd "env"; end
29
+ def pwd; cmd "pwd"; end
30
+ def cat(*args); cmd('cat', args); end
31
+ def grep(*args); cmd('grep', args); end
32
+ def date(*args); cmd('date', args); end
33
+ def ruby(*args); cmd('ruby', args); end
34
+ def perl(*args); cmd('perl', args); end
35
+ def bash(*args); cmd('bash', args); end
36
+ def echo(*args); cmd('echo', args); end
37
+ def sleep(seconds=1); cmd("sleep", seconds); end
38
+ def touch(*args); cmd('touch', args); end
39
+ def uname(*args); cmd('uname', args); end
40
+ def mount; cmd("mount"); end
41
+ def python(*args); cmd('python', args); end
42
+ def uptime; cmd("uptime"); end
43
+ def printenv(*args); cmd('printenv', args); end
44
+ # Consider Rye.sysinfo.os == :unix
45
+ end
46
+
47
+ end
data/lib/rye/rap.rb ADDED
@@ -0,0 +1,83 @@
1
+
2
+
3
+ module Rye;
4
+
5
+ # Rye::Rap
6
+ #
7
+ # This class is a modified Array which is returned by
8
+ # all command methods. The command output is split
9
+ # by line into an instance of this class. If there is
10
+ # only a single element it will act like a String.
11
+ #
12
+ # This class also contains a reference to the instance
13
+ # of Rye::Box or Rye::Set that the command was executed
14
+ # on.
15
+ #
16
+ class Rap < Array
17
+ # A reference to the Rye object instance the command
18
+ # was executed by (Rye::Box or Rye::Set)
19
+ attr_reader :obj
20
+
21
+ # An array containing any STDERR output
22
+ attr_reader :stderr
23
+
24
+ # * +obj+ an instance of Rye::Box or Rye::Set
25
+ # * +args+ anything that can sent to Array#new
26
+ def initialize(obj, *args)
27
+ @obj = obj
28
+ super *args
29
+ end
30
+
31
+ alias :box :obj
32
+ alias :set :obj
33
+
34
+ # Returns a reference to the Rye::Rap object (which
35
+ # acts like an Array that contains the STDOUT from the
36
+ # command executed over SSH). This is available to
37
+ # maintain consistency with the stderr method.
38
+ def stdout
39
+ self
40
+ end
41
+
42
+ # Add STDERR output from the command executed via SSH.
43
+ def add_stderr(*args)
44
+ args = args.flatten.compact
45
+ args = args.first.split($/) if args.size == 1
46
+ @stderr ||= []
47
+ @stderr << args
48
+ @stderr.flatten!
49
+ self
50
+ end
51
+
52
+ # Add STDOUT output from the command executed via SSH.
53
+ # This is available to maintain consistency with the
54
+ # add_stderr method. Otherwise there's no need to use
55
+ # this method (treat the Rye::Rap object like an Array).
56
+ def add_stdout(*args)
57
+ args = args.flatten.compact
58
+ args = args.first.split($/) if args.size == 1
59
+ self << args
60
+ self.flatten!
61
+ end
62
+
63
+ # Returns the first element if there it's the only
64
+ # one, otherwise the value of Array#to_s
65
+ def to_s
66
+ return self.first if self.size == 1
67
+ return "" if self.size == 0
68
+ super
69
+ end
70
+
71
+ #---
72
+ # If Box's shell methods return Rap objects, then
73
+ # we can do stuff like this
74
+ # rbox.cp '/etc' | rbox2['/tmp']
75
+ #def |(other)
76
+ # puts "BOX1", self.join($/)
77
+ # puts "BOX2", other.join($/)
78
+ #end
79
+ #+++
80
+
81
+ end
82
+
83
+ end
data/lib/rye/set.rb ADDED
@@ -0,0 +1,145 @@
1
+ module Rye
2
+
3
+ # = Rye::Set
4
+ #
5
+ #
6
+ class Set
7
+ attr_reader :name
8
+ attr_reader :boxes
9
+
10
+ # * +name+ The name of the set of machines
11
+ # * +opts+ a hash of optional arguments
12
+ #
13
+ # The +opts+ hash is used as defaults for all for all Rye::Box objects.
14
+ # All args supported by Rye::Box are available here with the addition of:
15
+ #
16
+ # * :parallel => run the commands in parallel? true or false (default).
17
+ #
18
+ def initialize(name='default', opts={})
19
+ @name = name
20
+ @boxes = []
21
+
22
+ # These opts are use by Rye::Box and also passed to Net::SSH
23
+ @opts = {
24
+ :parallel => false,
25
+ :user => Rye.sysinfo.user,
26
+ :safe => true,
27
+ :port => 22,
28
+ :keys => [],
29
+ :password => nil,
30
+ :proxy => nil,
31
+ :debug => nil,
32
+ :error => STDERR,
33
+ }.merge(opts)
34
+
35
+ @parallel = @opts.delete(:parallel) # Rye::Box doesn't have :parallel
36
+
37
+ @safe = @opts.delete(:safe)
38
+ @debug = @opts.delete(:debug)
39
+ @error = @opts.delete(:error)
40
+
41
+ add_keys(@opts[:keys])
42
+ end
43
+
44
+ # * +boxes+ one or more boxes. Rye::Box objects will be added directly
45
+ # to the set. Hostnames will be used to create new instances of Rye::Box
46
+ # and those will be added to the list.
47
+ def add_box(*boxes)
48
+ boxes = boxes.flatten.compact
49
+ @boxes += boxes.collect do |box|
50
+ box.is_a?(Rye::Box) ? box.add_keys(@keys) : Rye::Box.new(box, @opts)
51
+ end
52
+ @boxes
53
+ end
54
+ alias :add_boxes :add_box
55
+
56
+ # Add one or more private keys to the SSH Agent.
57
+ # * +additional_keys+ is a list of file paths to private keys
58
+ # Returns the instance of Rye::Set
59
+ def add_key(*additional_keys)
60
+ additional_keys = [additional_keys].flatten.compact || []
61
+ Rye.add_keys(additional_keys)
62
+ self
63
+ end
64
+ alias :add_keys :add_key
65
+
66
+ # Add an environment variable. +n+ and +v+ are the name and value.
67
+ # Returns the instance of Rye::Set
68
+ def add_env(n, v)
69
+ run_command(:add_env, n, v)
70
+ self
71
+ end
72
+ alias :add_environment_variable :add_env
73
+
74
+ # See Rye.keys
75
+ def keys
76
+ Rye.keys
77
+ end
78
+
79
+ # See Rye::Box.[]
80
+ def [](key=nil)
81
+ run_command(:cd, key)
82
+ self
83
+ end
84
+ alias :cd :'[]'
85
+
86
+ # Catches calls to Rye::Box commands. If +meth+ is the name of an
87
+ # instance method defined in Rye::Cmd then we call it against all
88
+ # the boxes in +@boxes+. Otherwise this method raises a
89
+ # Rye::CommandNotFound exception. It will also raise a Rye::NoBoxes
90
+ # exception if this set has no boxes defined.
91
+ #
92
+ # Returns a Rye::Rap object containing the responses from each Rye::Box.
93
+ def method_missing(meth, *args)
94
+ raise Rye::NoBoxes if @boxes.empty?
95
+ raise Rye::CommandNotFound, meth.to_s unless Rye::Cmd.instance_methods.member?(meth.to_s)
96
+ run_command(meth, *args)
97
+ end
98
+
99
+ private
100
+
101
+ # Determines whether to call the serial or parallel method, then calls it.
102
+ def run_command(meth, *args)
103
+ runner = @parallel ? :run_command_parallel : :run_command_serial
104
+ self.send(runner, meth, *args)
105
+ end
106
+
107
+
108
+ # Run the command on all boxes in parallel
109
+ def run_command_parallel(meth, *args)
110
+ debug "P: #{meth} on #{@boxes.size} boxes (#{@boxes.collect {|b| b.host }.join(', ')})"
111
+ threads = []
112
+
113
+ raps = Rye::Rap.new(self)
114
+ (@boxes || []).each do |box|
115
+ threads << Thread.new do
116
+ Thread.current[:rap] = box.send(meth, *args) # Store the result in the thread
117
+ end
118
+ end
119
+
120
+ threads.each do |t|
121
+ sleep 0.01 # Give the thread some breathing room
122
+ t.join # Wait for the thread to finish
123
+ raps << t[:rap] # Grab the result
124
+ end
125
+
126
+ raps
127
+ end
128
+
129
+
130
+ # Run the command on all boxes in serial
131
+ def run_command_serial(meth, *args)
132
+ debug "S: #{meth} on #{@boxes.size} boxes (#{@boxes.collect {|b| b.host }.join(', ')})"
133
+ raps = Rye::Rap.new(self)
134
+ (@boxes || []).each do |box|
135
+ raps << box.send(meth, *args)
136
+ end
137
+ raps
138
+ end
139
+
140
+ def debug(msg); @debug.puts msg if @debug; end
141
+ def error(msg); @error.puts msg if @error; end
142
+
143
+ end
144
+
145
+ end