sprinkle 0.4.2 → 0.5.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +54 -0
- data/README.markdown +178 -166
- data/Rakefile +4 -28
- data/bin/sprinkle +14 -1
- data/lib/sprinkle.rb +5 -1
- data/lib/sprinkle/actors/actors.rb +20 -5
- data/lib/sprinkle/actors/capistrano.rb +62 -36
- data/lib/sprinkle/actors/dummy.rb +127 -0
- data/lib/sprinkle/actors/local.rb +59 -17
- data/lib/sprinkle/actors/ssh.rb +189 -107
- data/lib/sprinkle/actors/vlad.rb +51 -32
- data/lib/sprinkle/configurable.rb +2 -1
- data/lib/sprinkle/deployment.rb +22 -2
- data/lib/sprinkle/errors/pretty_failure.rb +41 -0
- data/lib/sprinkle/errors/remote_command_failure.rb +24 -0
- data/lib/sprinkle/errors/transfer_failure.rb +28 -0
- data/lib/sprinkle/installers/apt.rb +17 -16
- data/lib/sprinkle/installers/binary.rb +23 -8
- data/lib/sprinkle/installers/brew.rb +17 -10
- data/lib/sprinkle/installers/bsd_port.rb +10 -6
- data/lib/sprinkle/installers/deb.rb +3 -10
- data/lib/sprinkle/installers/freebsd_pkg.rb +5 -11
- data/lib/sprinkle/installers/freebsd_portinstall.rb +8 -2
- data/lib/sprinkle/installers/gem.rb +9 -3
- data/lib/sprinkle/installers/group.rb +28 -4
- data/lib/sprinkle/installers/installer.rb +58 -7
- data/lib/sprinkle/installers/mac_port.rb +13 -6
- data/lib/sprinkle/installers/npm.rb +42 -0
- data/lib/sprinkle/installers/openbsd_pkg.rb +4 -11
- data/lib/sprinkle/installers/opensolaris_pkg.rb +7 -13
- data/lib/sprinkle/installers/package_installer.rb +33 -0
- data/lib/sprinkle/installers/pacman.rb +5 -13
- data/lib/sprinkle/installers/pear.rb +40 -0
- data/lib/sprinkle/installers/push_text.rb +18 -5
- data/lib/sprinkle/installers/rake.rb +7 -2
- data/lib/sprinkle/installers/reconnect.rb +29 -0
- data/lib/sprinkle/installers/replace_text.rb +11 -2
- data/lib/sprinkle/installers/rpm.rb +8 -6
- data/lib/sprinkle/installers/runner.rb +41 -16
- data/lib/sprinkle/installers/smart.rb +6 -17
- data/lib/sprinkle/installers/source.rb +22 -10
- data/lib/sprinkle/installers/thor.rb +7 -0
- data/lib/sprinkle/installers/transfer.rb +62 -41
- data/lib/sprinkle/installers/user.rb +34 -4
- data/lib/sprinkle/installers/yum.rb +10 -10
- data/lib/sprinkle/installers/zypper.rb +4 -15
- data/lib/sprinkle/package.rb +81 -98
- data/lib/sprinkle/policy.rb +11 -4
- data/lib/sprinkle/utility/log_recorder.rb +33 -0
- data/lib/sprinkle/verifiers/directory.rb +1 -1
- data/lib/sprinkle/verifiers/executable.rb +1 -1
- data/lib/sprinkle/verifiers/file.rb +11 -2
- data/lib/sprinkle/verifiers/package.rb +2 -14
- data/lib/sprinkle/verifiers/permission.rb +40 -0
- data/lib/sprinkle/verifiers/symlink.rb +2 -2
- data/lib/sprinkle/verifiers/test.rb +21 -0
- data/lib/sprinkle/verify.rb +3 -3
- data/lib/sprinkle/version.rb +3 -0
- data/spec/fixtures/my_file.txt +1 -0
- data/spec/sprinkle/actors/capistrano_spec.rb +16 -3
- data/spec/sprinkle/actors/local_spec.rb +24 -6
- data/spec/sprinkle/actors/ssh_spec.rb +38 -0
- data/spec/sprinkle/installers/apt_spec.rb +23 -2
- data/spec/sprinkle/installers/binary_spec.rb +22 -14
- data/spec/sprinkle/installers/brew_spec.rb +4 -4
- data/spec/sprinkle/installers/installer_spec.rb +36 -7
- data/spec/sprinkle/installers/npm_spec.rb +16 -0
- data/spec/sprinkle/installers/pear_spec.rb +16 -0
- data/spec/sprinkle/installers/push_text_spec.rb +23 -1
- data/spec/sprinkle/installers/rpm_spec.rb +5 -0
- data/spec/sprinkle/installers/runner_spec.rb +27 -11
- data/spec/sprinkle/installers/smart_spec.rb +60 -0
- data/spec/sprinkle/installers/source_spec.rb +4 -4
- data/spec/sprinkle/installers/transfer_spec.rb +31 -16
- data/spec/sprinkle/package_spec.rb +10 -2
- data/spec/sprinkle/policy_spec.rb +6 -0
- data/spec/sprinkle/verify_spec.rb +18 -4
- data/sprinkle.gemspec +22 -158
- metadata +178 -96
- data/TODO +0 -56
- data/VERSION +0 -1
- data/lib/sprinkle/verifiers/apt.rb +0 -21
- data/lib/sprinkle/verifiers/brew.rb +0 -21
- data/lib/sprinkle/verifiers/rpm.rb +0 -21
- data/lib/sprinkle/verifiers/users_groups.rb +0 -33
data/lib/sprinkle/actors/ssh.rb
CHANGED
@@ -3,148 +3,241 @@ require 'net/scp'
|
|
3
3
|
|
4
4
|
module Sprinkle
|
5
5
|
module Actors
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
# The SSH actor requires no additional deployment tools other than the
|
7
|
+
# Ruby SSH libraries.
|
8
|
+
#
|
9
|
+
# deployment do
|
10
|
+
# delivery :ssh do
|
11
|
+
# user "rails"
|
12
|
+
# password "leetz"
|
13
|
+
#
|
14
|
+
# role :app, "app.myserver.com"
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# == Working thru a gateway
|
20
|
+
#
|
21
|
+
# If you're behind a firewall and need to use a SSH gateway that's fine.
|
22
|
+
#
|
23
|
+
# deployment do
|
24
|
+
# delivery :ssh do
|
25
|
+
# gateway "work.sshgateway.com"
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
class SSH
|
29
|
+
attr_accessor :options #:nodoc:
|
30
|
+
|
31
|
+
class SSHCommandFailure < StandardError #:nodoc:
|
32
|
+
attr_accessor :details
|
33
|
+
end
|
34
|
+
|
35
|
+
class SSHConnectionCache
|
36
|
+
def initialize; @cache={}; end
|
37
|
+
def start(host, user, opts={})
|
38
|
+
key="#{host}#{user}#{opts.to_s}"
|
39
|
+
@cache[key] ||= Net::SSH.start(host,user,opts)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
9
44
|
def initialize(options = {}, &block) #:nodoc:
|
10
45
|
@options = options.update(:user => 'root')
|
46
|
+
@roles = {}
|
47
|
+
@connection_cache = SSHConnectionCache.new
|
11
48
|
self.instance_eval &block if block
|
49
|
+
raise "You must define at least a single role." if @roles.empty?
|
12
50
|
end
|
13
51
|
|
52
|
+
# Define a whole host of roles at once
|
53
|
+
#
|
54
|
+
# This is depreciated - you should be using role instead.
|
14
55
|
def roles(roles)
|
15
|
-
@
|
56
|
+
@roles = roles
|
57
|
+
end
|
58
|
+
|
59
|
+
# Define a role and add servers to it
|
60
|
+
#
|
61
|
+
# role :app, "app.server.com"
|
62
|
+
# role :db, "db.server.com"
|
63
|
+
def role(role, server)
|
64
|
+
@roles[role] ||= []
|
65
|
+
@roles[role] << server
|
16
66
|
end
|
17
67
|
|
68
|
+
# Set an optional SSH gateway server - if set all outbound SSH traffic
|
69
|
+
# will go thru this gateway
|
18
70
|
def gateway(gateway)
|
19
71
|
@options[:gateway] = gateway
|
20
72
|
end
|
21
73
|
|
74
|
+
# Set the SSH user
|
22
75
|
def user(user)
|
23
76
|
@options[:user] = user
|
24
77
|
end
|
25
78
|
|
79
|
+
# Set the SSH password
|
26
80
|
def password(password)
|
27
81
|
@options[:password] = password
|
28
82
|
end
|
29
83
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
logger.debug green "process returning #{r}"
|
34
|
-
return r
|
84
|
+
# Set this to true to prepend 'sudo' to every command.
|
85
|
+
def use_sudo(value)
|
86
|
+
@options[:use_sudo] = value
|
35
87
|
end
|
36
88
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
89
|
+
def setup_gateway #:nodoc:
|
90
|
+
@gateway ||= Net::SSH::Gateway.new(@options[:gateway], @options[:user]) if @options[:gateway]
|
91
|
+
end
|
92
|
+
|
93
|
+
def teardown #:nodoc:
|
94
|
+
@gateway.shutdown! if @gateway
|
95
|
+
end
|
96
|
+
|
97
|
+
def verify(verifier, roles, opts = {}) #:nodoc:
|
98
|
+
@verifier = verifier
|
99
|
+
# issue all the verification steps in a single SSH command
|
100
|
+
commands=[verifier.commands.join(" && ")]
|
101
|
+
process(verifier.package.name, commands, roles,
|
102
|
+
:suppress_and_return_failures => true)
|
103
|
+
ensure
|
104
|
+
@verifier = nil
|
105
|
+
end
|
106
|
+
|
107
|
+
def install(installer, roles, opts = {}) #:nodoc:
|
108
|
+
@installer = installer
|
109
|
+
process(installer.package.name, installer.install_sequence, roles)
|
110
|
+
rescue SSHCommandFailure => e
|
111
|
+
raise_error(e)
|
112
|
+
ensure
|
113
|
+
@installer = nil
|
40
114
|
end
|
41
|
-
|
115
|
+
|
42
116
|
protected
|
43
117
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
118
|
+
def raise_error(e)
|
119
|
+
raise Sprinkle::Errors::RemoteCommandFailure.new(@installer, e.details, e)
|
120
|
+
end
|
121
|
+
|
122
|
+
def process(name, commands, roles, opts = {}) #:nodoc:
|
123
|
+
opts.reverse_merge!(:suppress_and_return_failures => false)
|
124
|
+
setup_gateway
|
125
|
+
@suppress = opts[:suppress_and_return_failures]
|
126
|
+
r=execute_on_role(commands, roles)
|
127
|
+
logger.debug green "process returning #{r}"
|
128
|
+
return r
|
129
|
+
end
|
130
|
+
|
131
|
+
def execute_on_role(commands, role) #:nodoc:
|
132
|
+
hosts = @roles[role]
|
133
|
+
Array(hosts).each do |host|
|
134
|
+
success = execute_on_host(commands, host)
|
135
|
+
return false unless success
|
48
136
|
end
|
49
|
-
!(res.include? false)
|
50
137
|
end
|
51
138
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
139
|
+
def prepare_commands(commands)
|
140
|
+
return commands unless @options[:use_sudo]
|
141
|
+
commands.map do |command|
|
142
|
+
next command if command.is_a?(Symbol)
|
143
|
+
command.match(/^sudo/) ? command : "sudo #{command}"
|
144
|
+
end
|
56
145
|
end
|
57
146
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
147
|
+
def execute_on_host(commands,host) #:nodoc:
|
148
|
+
session = ssh_session(host)
|
149
|
+
@log_recorder = Sprinkle::Utility::LogRecorder.new
|
150
|
+
prepare_commands(commands).each do |cmd|
|
151
|
+
if cmd == :TRANSFER
|
152
|
+
transfer_to_host(@installer.sourcepath, @installer.destination, session,
|
153
|
+
:recursive => @installer.options[:recursive])
|
154
|
+
next
|
155
|
+
elsif cmd == :RECONNECT
|
156
|
+
session.close # disconnenct
|
157
|
+
session = ssh_session(host) # reconnect
|
158
|
+
next
|
159
|
+
end
|
160
|
+
@log_recorder.reset cmd
|
161
|
+
res = ssh(session, cmd)
|
162
|
+
if res != 0
|
163
|
+
if @suppress
|
164
|
+
return false
|
165
|
+
else
|
166
|
+
fail=SSHCommandFailure.new
|
167
|
+
fail.details = @log_recorder.hash.merge(:hosts => host)
|
168
|
+
raise fail, "#{cmd} failed with error code #{res[:code]}"
|
169
|
+
end
|
170
|
+
end
|
61
171
|
end
|
172
|
+
true
|
62
173
|
end
|
63
174
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
def execute_on_role(commands, role, gateway = nil)
|
69
|
-
hosts = @options[:roles][role]
|
70
|
-
res = []
|
71
|
-
Array(hosts).each { |host| res << execute_on_host(commands, host, gateway) }
|
72
|
-
!(res.include? false)
|
73
|
-
end
|
74
|
-
|
75
|
-
def transfer_to_role(source, destination, role, gateway = nil)
|
76
|
-
hosts = @options[:roles][role]
|
77
|
-
Array(hosts).each { |host| transfer_to_host(source, destination, host, gateway) }
|
175
|
+
def ssh(host, cmd, opts={}) #:nodoc:
|
176
|
+
logger.debug "ssh: #{cmd}"
|
177
|
+
session = host.is_a?(Net::SSH::Connection::Session) ? host : ssh_session(host)
|
178
|
+
channel_runner(session, cmd)
|
78
179
|
end
|
79
180
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
res = execute_on_connection(commands, ssh)
|
86
|
-
ssh.loop
|
181
|
+
def channel_runner(session, command) #:nodoc:
|
182
|
+
session.open_channel do |channel|
|
183
|
+
channel.on_data do |ch, data|
|
184
|
+
@log_recorder.log :out, data
|
185
|
+
logger.debug yellow("stdout said-->\n#{data}\n")
|
87
186
|
end
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
187
|
+
channel.on_extended_data do |ch, type, data|
|
188
|
+
next unless type == 1 # only handle stderr
|
189
|
+
@log_recorder.log :err, data
|
190
|
+
logger.debug red("stderr said -->\n#{data}\n")
|
92
191
|
end
|
93
|
-
end
|
94
|
-
res.detect{|x| x!=0}.nil?
|
95
|
-
end
|
96
|
-
|
97
|
-
def execute_on_connection(commands, session)
|
98
|
-
res = []
|
99
|
-
Array(commands).each do |cmd|
|
100
|
-
session.open_channel do |channel|
|
101
|
-
channel.on_data do |ch, data|
|
102
|
-
logger.debug yellow("stdout said-->\n#{data}\n")
|
103
|
-
end
|
104
|
-
channel.on_extended_data do |ch, type, data|
|
105
|
-
next unless type == 1 # only handle stderr
|
106
|
-
logger.debug red("stderr said -->\n#{data}\n")
|
107
|
-
end
|
108
192
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
end
|
116
|
-
res << exit_code
|
117
|
-
end
|
118
|
-
|
119
|
-
channel.on_request("exit-signal") do |ch, data|
|
120
|
-
logger.debug red("#{cmd} was signaled!: #{data.read_long}")
|
121
|
-
end
|
122
|
-
|
123
|
-
channel.exec cmd do |ch, status|
|
124
|
-
logger.error("couldn't run remote command #{cmd}") unless status
|
193
|
+
channel.on_request("exit-status") do |ch, data|
|
194
|
+
@log_recorder.code = data.read_long
|
195
|
+
if @log_recorder.code == 0
|
196
|
+
logger.debug(green 'success')
|
197
|
+
else
|
198
|
+
logger.debug(red('failed (%d).' % @log_recorder.code))
|
125
199
|
end
|
126
200
|
end
|
127
|
-
end
|
128
|
-
res
|
129
|
-
end
|
130
201
|
|
131
|
-
|
132
|
-
|
133
|
-
gateway.ssh(host, @options[:user]) do |ssh|
|
134
|
-
transfer_on_connection(source, destination, recursive, ssh)
|
202
|
+
channel.on_request("exit-signal") do |ch, data|
|
203
|
+
logger.debug red("#{cmd} was signaled!: #{data.read_long}")
|
135
204
|
end
|
136
|
-
|
137
|
-
|
138
|
-
|
205
|
+
|
206
|
+
channel.exec command do |ch, status|
|
207
|
+
logger.error("couldn't run remote command #{cmd}") unless status
|
208
|
+
@log_recorder.code = -1
|
139
209
|
end
|
140
210
|
end
|
211
|
+
session.loop
|
212
|
+
@log_recorder.code
|
141
213
|
end
|
142
|
-
|
143
|
-
def
|
144
|
-
|
145
|
-
|
214
|
+
|
215
|
+
def transfer_to_role(source, destination, role, opts={}) #:nodoc:
|
216
|
+
hosts = @roles[role]
|
217
|
+
Array(hosts).each { |host| transfer_to_host(source, destination, host, opts) }
|
146
218
|
end
|
147
|
-
|
219
|
+
|
220
|
+
def transfer_to_host(source, destination, host, opts={}) #:nodoc:
|
221
|
+
logger.debug "upload: #{destination}"
|
222
|
+
session = host.is_a?(Net::SSH::Connection::Session) ? host : ssh_session(host)
|
223
|
+
scp = Net::SCP.new(session)
|
224
|
+
scp.upload! source, destination, :recursive => opts[:recursive], :chunk_size => 32.kilobytes
|
225
|
+
rescue RuntimeError => e
|
226
|
+
if e.message =~ /Permission denied/
|
227
|
+
raise TransferFailure.no_permission(@installer,e)
|
228
|
+
else
|
229
|
+
raise e
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def ssh_session(host)
|
234
|
+
if @gateway
|
235
|
+
gateway.ssh(host, @options[:user])
|
236
|
+
else
|
237
|
+
@connection_cache.start(host, @options[:user],:password => @options[:password])
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
148
241
|
private
|
149
242
|
def color(code, s)
|
150
243
|
"\033[%sm%s\033[0m"%[code,s]
|
@@ -161,17 +254,6 @@ module Sprinkle
|
|
161
254
|
def blue(s)
|
162
255
|
color(34, s)
|
163
256
|
end
|
164
|
-
|
165
|
-
def gateway_defined?
|
166
|
-
!! @options[:gateway]
|
167
|
-
end
|
168
|
-
|
169
|
-
def on_gateway(&block)
|
170
|
-
gateway = Net::SSH::Gateway.new(@options[:gateway], @options[:user])
|
171
|
-
block.call gateway
|
172
|
-
ensure
|
173
|
-
gateway.shutdown!
|
174
|
-
end
|
175
257
|
end
|
176
258
|
end
|
177
259
|
end
|
data/lib/sprinkle/actors/vlad.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
+
require 'vlad'
|
2
|
+
|
1
3
|
module Sprinkle
|
2
4
|
module Actors
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# Vlad is one of the delivery method options available out of the
|
6
|
-
# box with Sprinkle. If you have the vlad the deployer gem install, you
|
5
|
+
# The Vlad actor is one of the delivery method options available out of the
|
6
|
+
# box with Sprinkle. If you have the vlad the deployer gem installed, you
|
7
7
|
# may use this delivery. The only configuration option available, and
|
8
8
|
# which is mandatory to include is +script+. An example:
|
9
9
|
#
|
@@ -17,7 +17,6 @@ module Sprinkle
|
|
17
17
|
# These recipes are mainly to set variables such as :user, :password, and to
|
18
18
|
# set the app domain which will be sprinkled.
|
19
19
|
class Vlad
|
20
|
-
require 'vlad'
|
21
20
|
attr_accessor :loaded_recipes #:nodoc:
|
22
21
|
|
23
22
|
def initialize(&block) #:nodoc:
|
@@ -39,38 +38,58 @@ module Sprinkle
|
|
39
38
|
require name
|
40
39
|
@loaded_recipes << name
|
41
40
|
end
|
42
|
-
|
43
|
-
def
|
44
|
-
|
45
|
-
if
|
46
|
-
|
41
|
+
|
42
|
+
def install(installer, roles, opts={})
|
43
|
+
@installer=installer
|
44
|
+
if installer.install_sequence.include?(:TRANSFER)
|
45
|
+
process_with_transfer(installer.package.name, installer.install_sequence, roles, opts)
|
46
|
+
else
|
47
|
+
process(installer.package.name, installer.install_sequence, roles, opts)
|
47
48
|
end
|
49
|
+
# recast our rake error to the common sprinkle error type
|
50
|
+
rescue ::Rake::CommandFailedError => e
|
51
|
+
raise Sprinkle::Errors::RemoteCommandFailure.new(installer, {}, e)
|
52
|
+
ensure
|
53
|
+
@installer = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def verify(verifier, roles, opts={})
|
57
|
+
process(verifier.package.name, commands, roles,
|
58
|
+
:suppress_and_return_failures => true)
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def process(name, commands, roles, opts ={}) #:nodoc:
|
64
|
+
commands = commands.map{|x| "sudo #{x}"} if use_sudo
|
48
65
|
commands = commands.join(' && ')
|
49
66
|
puts "executing #{commands}"
|
50
|
-
|
51
|
-
|
52
|
-
begin
|
53
|
-
t.invoke
|
54
|
-
return true
|
55
|
-
rescue ::Rake::CommandFailedError => e
|
56
|
-
return false if suppress_and_return_failures
|
57
|
-
|
58
|
-
# Reraise error if we're not suppressing it
|
59
|
-
raise
|
60
|
-
end
|
67
|
+
task = remote_task(task_sym(name), :roles => roles) { run commands }
|
68
|
+
invoke(task)
|
61
69
|
end
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
70
|
+
|
71
|
+
def process_with_transfer(name, commands, roles, opts ={}) #:nodoc:
|
72
|
+
raise "cant do non recursive file transfers, sorry" if opts[:recursive] == false
|
73
|
+
commands = commands.map{|x| x == :TRANSFER : x ? "sudo #{x}" } if use_sudo
|
74
|
+
i = commands.index(:TRANSFER)
|
75
|
+
before = commands.first(i).join(" && ")
|
76
|
+
after = commands.last(commands.size-i+1).join(" && ")
|
77
|
+
inst = @installer
|
78
|
+
task = remote_task(task_sym(name), :roles => roles) do
|
79
|
+
run before unless before.empty?
|
80
|
+
rsync inst.sourcepath, inst.destination
|
81
|
+
run after unless after.empty?
|
73
82
|
end
|
83
|
+
invoke(task)
|
84
|
+
end
|
85
|
+
|
86
|
+
def invoke(t)
|
87
|
+
t.invoke
|
88
|
+
return true
|
89
|
+
rescue ::Rake::CommandFailedError => e
|
90
|
+
return false if opts[:suppress_and_return_failures]
|
91
|
+
# Reraise error if we're not suppressing it
|
92
|
+
raise e
|
74
93
|
end
|
75
94
|
|
76
95
|
private
|