rye 0.3.2 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +20 -0
- data/README.rdoc +68 -22
- data/bin/rye +131 -0
- data/bin/try +40 -17
- data/lib/rye.rb +150 -13
- data/lib/rye/box.rb +141 -125
- data/lib/rye/cmd.rb +23 -0
- data/lib/rye/key.rb +134 -0
- data/lib/rye/rap.rb +9 -0
- data/lib/rye/set.rb +9 -0
- data/rye.gemspec +14 -3
- data/try/copying.rb +19 -0
- data/try/keys.rb +139 -0
- data/tst/10-key1 +27 -0
- data/tst/10-key1.pub +1 -0
- data/tst/10-key2 +30 -0
- data/tst/10-key2.pub +1 -0
- data/tst/10_keys_test.rb +88 -0
- data/{test/10_rye_test.rb → tst/50_rye_test.rb} +11 -20
- metadata +34 -5
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
|
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
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
#
|
191
|
-
#
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
#
|
207
|
-
#
|
208
|
-
#
|
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:
|
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
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
226
|
-
|
227
|
-
|
228
|
-
def
|
229
|
-
|
230
|
-
|
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
|
-
#
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
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
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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
|