sprinkle 0.4.2 → 0.5.0.rc1
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/.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
|