rye 0.3

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 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