beaker 1.7.0 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/README.md +14 -375
- data/lib/beaker.rb +1 -1
- data/lib/beaker/answers.rb +1 -1
- data/lib/beaker/answers/version20.rb +1 -0
- data/lib/beaker/answers/version28.rb +2 -1
- data/lib/beaker/answers/version30.rb +1 -0
- data/lib/beaker/answers/version32.rb +3 -1
- data/lib/beaker/dsl/helpers.rb +24 -3
- data/lib/beaker/dsl/install_utils.rb +6 -4
- data/lib/beaker/host.rb +1 -2
- data/lib/beaker/hypervisor/fusion.rb +10 -2
- data/lib/beaker/hypervisor/vcloud_pooled.rb +3 -0
- data/lib/beaker/options/parser.rb +19 -21
- data/lib/beaker/options/presets.rb +3 -1
- data/lib/beaker/platform.rb +59 -0
- data/lib/beaker/ssh_connection.rb +2 -0
- data/lib/beaker/utils/ntp_control.rb +26 -11
- data/lib/beaker/utils/validator.rb +14 -5
- data/lib/beaker/version.rb +1 -1
- data/spec/beaker/answers_spec.rb +39 -1
- data/spec/beaker/dsl/helpers_spec.rb +30 -5
- data/spec/beaker/options/parser_spec.rb +4 -25
- data/spec/beaker/platform_spec.rb +97 -0
- data/spec/beaker/utils/ntp_control_spec.rb +28 -0
- data/spec/beaker/utils/validator_spec.rb +33 -0
- data/spec/helpers.rb +3 -2
- data/spec/mocks.rb +5 -0
- metadata +18 -15
data/lib/beaker/dsl/helpers.rb
CHANGED
@@ -70,7 +70,10 @@ module Beaker
|
|
70
70
|
# @raise [FailTest] Raises an exception if *command* obviously fails.
|
71
71
|
def on(host, command, opts = {}, &block)
|
72
72
|
unless command.is_a? Command
|
73
|
-
cmd_opts =
|
73
|
+
cmd_opts = {}
|
74
|
+
if opts[:environment]
|
75
|
+
cmd_opts['ENV'] = opts[:environment]
|
76
|
+
end
|
74
77
|
command = Command.new(command.to_s, [], cmd_opts)
|
75
78
|
end
|
76
79
|
if host.is_a? String or host.is_a? Symbol
|
@@ -82,7 +85,16 @@ module Beaker
|
|
82
85
|
@result = host.exec(command, opts)
|
83
86
|
|
84
87
|
# Also, let additional checking be performed by the caller.
|
85
|
-
|
88
|
+
if block_given?
|
89
|
+
case block.arity
|
90
|
+
#block with arity of 0, just hand back yourself
|
91
|
+
when 0
|
92
|
+
yield self
|
93
|
+
#block with arity of 1 or greater, hand back the result object
|
94
|
+
else
|
95
|
+
yield @result
|
96
|
+
end
|
97
|
+
end
|
86
98
|
|
87
99
|
return @result
|
88
100
|
end
|
@@ -453,7 +465,8 @@ module Beaker
|
|
453
465
|
# @api dsl
|
454
466
|
def with_puppet_running_on host, conf_opts, testdir = host.tmpdir(File.basename(@path)), &block
|
455
467
|
raise(ArgumentError, "with_puppet_running_on's conf_opts must be a Hash. You provided a #{conf_opts.class}: '#{conf_opts}'") if !conf_opts.kind_of?(Hash)
|
456
|
-
cmdline_args = conf_opts
|
468
|
+
cmdline_args = conf_opts[:__commandline_args__]
|
469
|
+
conf_opts = conf_opts.reject { |k,v| k == :__commandline_args__ }
|
457
470
|
|
458
471
|
begin
|
459
472
|
backup_file = backup_the_file(host, host['puppetpath'], testdir, 'puppet.conf')
|
@@ -486,6 +499,14 @@ module Beaker
|
|
486
499
|
end
|
487
500
|
|
488
501
|
rescue Exception => teardown_exception
|
502
|
+
begin
|
503
|
+
if !host.is_pe?
|
504
|
+
dump_puppet_log(host)
|
505
|
+
end
|
506
|
+
rescue Exception => dumping_exception
|
507
|
+
logger.error("Raised during attempt to dump puppet logs: #{dumping_exception}")
|
508
|
+
end
|
509
|
+
|
489
510
|
if original_exception
|
490
511
|
logger.error("Raised during attempt to teardown with_puppet_running_on: #{teardown_exception}\n---\n")
|
491
512
|
raise original_exception
|
@@ -453,10 +453,10 @@ module Beaker
|
|
453
453
|
hosts.each do |host|
|
454
454
|
host['pe_dir'] ||= options[:pe_dir]
|
455
455
|
if host['platform'] =~ /windows/
|
456
|
-
host['pe_ver'] = host['pe_ver'] ||
|
456
|
+
host['pe_ver'] = host['pe_ver'] || options['pe_ver'] ||
|
457
457
|
Beaker::Options::PEVersionScraper.load_pe_version(host[:pe_dir], options[:pe_version_file_win])
|
458
458
|
else
|
459
|
-
host['pe_ver'] = host['pe_ver'] ||
|
459
|
+
host['pe_ver'] = host['pe_ver'] || options['pe_ver'] ||
|
460
460
|
Beaker::Options::PEVersionScraper.load_pe_version(host[:pe_dir], options[:pe_version_file])
|
461
461
|
end
|
462
462
|
end
|
@@ -478,9 +478,11 @@ module Beaker
|
|
478
478
|
hosts.each do |host|
|
479
479
|
host['pe_dir'] = host['pe_upgrade_dir'] || path
|
480
480
|
if host['platform'] =~ /windows/
|
481
|
-
host['pe_ver'] = host['pe_upgrade_ver'] ||
|
481
|
+
host['pe_ver'] = host['pe_upgrade_ver'] || options['pe_upgrade_ver'] ||
|
482
|
+
Options::PEVersionScraper.load_pe_version(host['pe_dir'], options[:pe_version_file_win])
|
482
483
|
else
|
483
|
-
host['pe_ver'] = host['pe_upgrade_ver'] ||
|
484
|
+
host['pe_ver'] = host['pe_upgrade_ver'] || options['pe_upgrade_ver'] ||
|
485
|
+
Options::PEVersionScraper.load_pe_version(host['pe_dir'], options[:pe_version_file])
|
484
486
|
end
|
485
487
|
if version_is_less(host['pe_ver'], '3.0')
|
486
488
|
host['pe_installer'] ||= 'puppet-enterprise-upgrader'
|
data/lib/beaker/host.rb
CHANGED
@@ -180,8 +180,7 @@ module Beaker
|
|
180
180
|
# exit codes at the host level and then raising...
|
181
181
|
# is it necessary to break execution??
|
182
182
|
unless result.exit_code_in?(Array(options[:acceptable_exit_codes] || 0))
|
183
|
-
|
184
|
-
raise CommandFailure, "Host '#{self}' exited with #{result.exit_code} running:\n #{cmdline}\nLast #{limit} lines of output were:\n#{result.formatted_output(limit)}"
|
183
|
+
raise CommandFailure, "Host '#{self}' exited with #{result.exit_code} running:\n #{cmdline}\nLast #{@options[:trace_limit]} lines of output were:\n#{result.formatted_output(@options[:trace_limit])}"
|
185
184
|
end
|
186
185
|
end
|
187
186
|
# Danger, so we have to return this result?
|
@@ -11,6 +11,10 @@ module Beaker
|
|
11
11
|
@logger = options[:logger]
|
12
12
|
@options = options
|
13
13
|
@fusion_hosts = fusion_hosts
|
14
|
+
#check preconditions for fusion
|
15
|
+
@fusion_hosts.each do |host|
|
16
|
+
raise "You must specify a snapshot for Fusion instances, no snapshot defined for #{host.name}!" unless host["snapshot"]
|
17
|
+
end
|
14
18
|
@fission = Fission::VM
|
15
19
|
end
|
16
20
|
|
@@ -23,10 +27,14 @@ module Beaker
|
|
23
27
|
vm = @fission.new vm_name
|
24
28
|
raise "Could not find VM '#{vm_name}' for #{host.name}!" unless vm.exists?
|
25
29
|
|
26
|
-
|
30
|
+
vm_snapshots = vm.snapshots.data
|
31
|
+
if vm_snapshots.nil? or vm_snapshots.empty?
|
32
|
+
raise "No snapshots available for VM #{host.name} (vmname: '#{vm_name}')"
|
33
|
+
end
|
34
|
+
|
35
|
+
available_snapshots = vm_snapshots.sort.join(", ")
|
27
36
|
@logger.notify "Available snapshots for #{host.name}: #{available_snapshots}"
|
28
37
|
snap_name = host["snapshot"]
|
29
|
-
raise "No snapshot specified for #{host.name}" unless snap_name
|
30
38
|
raise "Could not find snapshot '#{snap_name}' for host #{host.name}!" unless vm.snapshots.data.include? snap_name
|
31
39
|
|
32
40
|
@logger.notify "Reverting #{host.name} to snapshot '#{snap_name}'"
|
@@ -55,6 +55,9 @@ module Beaker
|
|
55
55
|
start = Time.now
|
56
56
|
try = 1
|
57
57
|
@vcloud_hosts.each_with_index do |h, i|
|
58
|
+
if not h['template']
|
59
|
+
raise ArgumentError, "You must specify a template name for #{h}"
|
60
|
+
end
|
58
61
|
if h['template'] =~ /\//
|
59
62
|
templatefolders = h['template'].split('/')
|
60
63
|
h['template'] = templatefolders.pop
|
@@ -5,14 +5,12 @@ module Beaker
|
|
5
5
|
#An Object that parses, merges and normalizes all supported Beaker options and arguments
|
6
6
|
class Parser
|
7
7
|
GITREPO = 'git://github.com/puppetlabs'
|
8
|
-
#These options can have the form of arg1,arg2 or [arg] or just arg,
|
8
|
+
#These options can have the form of arg1,arg2 or [arg] or just arg,
|
9
9
|
#should default to []
|
10
10
|
LONG_OPTS = [:helper, :load_path, :tests, :pre_suite, :post_suite, :install, :modules]
|
11
11
|
#These options expand out into an array of .rb files
|
12
12
|
RB_FILE_OPTS = [:tests, :pre_suite, :post_suite]
|
13
13
|
|
14
|
-
PLATFORMS = /^(centos|fedora|debian|oracle|redhat|scientific|sles|ubuntu|windows|solaris|aix|el)\-.+\-.+$/
|
15
|
-
|
16
14
|
PARSE_ERROR = if RUBY_VERSION > '1.8.7'; then Psych::SyntaxError; else ArgumentError; end
|
17
15
|
|
18
16
|
#The OptionsHash of all parsed options
|
@@ -41,7 +39,7 @@ module Beaker
|
|
41
39
|
# or can become an array of multiple values by splitting arg over ','. If argument is already an
|
42
40
|
# array that array is returned untouched.
|
43
41
|
# @example
|
44
|
-
# split_arg([1, 2, 3]) == [1, 2, 3]
|
42
|
+
# split_arg([1, 2, 3]) == [1, 2, 3]
|
45
43
|
# split_arg(1) == [1]
|
46
44
|
# split_arg("1,2") == ["1", "2"]
|
47
45
|
# split_arg(nil) == []
|
@@ -71,9 +69,9 @@ module Beaker
|
|
71
69
|
files = []
|
72
70
|
if not paths.empty?
|
73
71
|
paths.each do |root|
|
74
|
-
if File.file?
|
72
|
+
if File.file?(root)
|
75
73
|
files << root
|
76
|
-
|
74
|
+
elsif File.directory?(root) #expand and explore
|
77
75
|
discover_files = Dir.glob(
|
78
76
|
File.join(root, "**/*.rb")
|
79
77
|
).select { |f| File.file?(f) }
|
@@ -81,6 +79,8 @@ module Beaker
|
|
81
79
|
parser_error "empty directory used as an option (#{root})!"
|
82
80
|
end
|
83
81
|
files += discover_files.sort
|
82
|
+
else #not a file, not a directory, not nothin'
|
83
|
+
parser_error "#{root} used as a file option but is not a file or directory!"
|
84
84
|
end
|
85
85
|
end
|
86
86
|
end
|
@@ -93,9 +93,9 @@ module Beaker
|
|
93
93
|
#Converts array of paths into array of fully qualified git repo URLS with expanded keywords
|
94
94
|
#
|
95
95
|
#Supports the following keywords
|
96
|
-
# PUPPET
|
96
|
+
# PUPPET
|
97
97
|
# FACTER
|
98
|
-
# HIERA
|
98
|
+
# HIERA
|
99
99
|
# HIERA-PUPPET
|
100
100
|
#@example
|
101
101
|
# opts = ["PUPPET/3.1"]
|
@@ -142,7 +142,7 @@ module Beaker
|
|
142
142
|
#NOTE on argument precedence:
|
143
143
|
#
|
144
144
|
# Will use env, then hosts/config file, then command line, then file options
|
145
|
-
#
|
145
|
+
#
|
146
146
|
@options = Beaker::Options::Presets.presets
|
147
147
|
cmd_line_options = @command_line_parser.parse!(args)
|
148
148
|
file_options = Beaker::Options::OptionsFileParser.parse_options_file(cmd_line_options[:options_file])
|
@@ -150,7 +150,7 @@ module Beaker
|
|
150
150
|
# overwrite file options with command line options
|
151
151
|
cmd_line_and_file_options = file_options.merge(cmd_line_options)
|
152
152
|
# merge command line and file options with defaults
|
153
|
-
# overwrite defaults with command line and file options
|
153
|
+
# overwrite defaults with command line and file options
|
154
154
|
@options = @options.merge(cmd_line_and_file_options)
|
155
155
|
|
156
156
|
if not @options[:help] and not @options[:version]
|
@@ -206,9 +206,7 @@ module Beaker
|
|
206
206
|
if not @options['HOSTS'][name]['platform']
|
207
207
|
parser_error "Host #{name} does not have a platform specified"
|
208
208
|
else
|
209
|
-
|
210
|
-
parser_error "Host #{name} is on unsupported platform #{@options['HOSTS'][name]['platform']}"
|
211
|
-
end
|
209
|
+
@options['HOSTS'][name]['platform'] = Platform.new(@options['HOSTS'][name]['platform'])
|
212
210
|
end
|
213
211
|
end
|
214
212
|
|
@@ -231,8 +229,8 @@ module Beaker
|
|
231
229
|
else
|
232
230
|
@options[opt] = []
|
233
231
|
end
|
234
|
-
end
|
235
|
-
|
232
|
+
end
|
233
|
+
|
236
234
|
#check for valid type
|
237
235
|
if @options[:type] !~ /(pe)|(git)|(foss)/
|
238
236
|
parser_error "--type must be one of pe, git, or foss, not '#{@options[:type]}'"
|
@@ -240,7 +238,7 @@ module Beaker
|
|
240
238
|
|
241
239
|
#check for valid fail mode
|
242
240
|
if @options[:fail_mode] !~ /stop|fast|slow/
|
243
|
-
parser_error "--fail-mode must be one of fast or slow, not '#{@options[:fail_mode]}'"
|
241
|
+
parser_error "--fail-mode must be one of fast or slow, not '#{@options[:fail_mode]}'"
|
244
242
|
end
|
245
243
|
|
246
244
|
#check for valid preserve_hosts option
|
@@ -249,8 +247,8 @@ module Beaker
|
|
249
247
|
end
|
250
248
|
|
251
249
|
#check for config files necessary for different hypervisors
|
252
|
-
hypervisors = []
|
253
|
-
@options[:HOSTS].each_key do |name|
|
250
|
+
hypervisors = []
|
251
|
+
@options[:HOSTS].each_key do |name|
|
254
252
|
hypervisors << @options[:HOSTS][name][:hypervisor].to_s
|
255
253
|
end
|
256
254
|
hypervisors.uniq!
|
@@ -266,7 +264,7 @@ module Beaker
|
|
266
264
|
#check that roles of hosts make sense
|
267
265
|
# - must be one and only one master
|
268
266
|
roles = []
|
269
|
-
@options[:HOSTS].each_key do |name|
|
267
|
+
@options[:HOSTS].each_key do |name|
|
270
268
|
roles << @options[:HOSTS][name][:roles]
|
271
269
|
end
|
272
270
|
master = 0
|
@@ -299,8 +297,8 @@ module Beaker
|
|
299
297
|
# @api private
|
300
298
|
def test_host_roles(host_name, host_hash)
|
301
299
|
host_roles = host_hash[:roles]
|
302
|
-
if (host_roles
|
303
|
-
parser_error "#{host_hash[:platform].to_s} box '#{host_name}'
|
300
|
+
if !(host_roles & ['master', 'database', 'dashboard']).empty?
|
301
|
+
parser_error "#{host_hash[:platform].to_s} box '#{host_name}' may not have roles 'master', 'dashboard', or 'database'; it has roles #{host_roles.to_s}"
|
304
302
|
end
|
305
303
|
end
|
306
304
|
|
@@ -8,7 +8,7 @@ module Beaker
|
|
8
8
|
#
|
9
9
|
# Currently supports:
|
10
10
|
#
|
11
|
-
# consoleport, IS_PE, pe_dist_dir, pe_version_file, pe_version_file_win
|
11
|
+
# consoleport, IS_PE, pe_dist_dir, pe_version_file, pe_version_file_win, pe_ver
|
12
12
|
#
|
13
13
|
# @return [OptionsHash] The supported environment variables in an OptionsHash,
|
14
14
|
# empty or nil environment variables are removed from the OptionsHash
|
@@ -20,6 +20,7 @@ module Beaker
|
|
20
20
|
:pe_dir => ENV['pe_dist_dir'],
|
21
21
|
:pe_version_file => ENV['pe_version_file'],
|
22
22
|
:pe_version_file_win => ENV['pe_version_file'],
|
23
|
+
:pe_ver => ENV['pe_ver']
|
23
24
|
}.delete_if {|key, value| value.nil? or value.empty? })
|
24
25
|
end
|
25
26
|
|
@@ -30,6 +31,7 @@ module Beaker
|
|
30
31
|
h = Beaker::Options::OptionsHash.new
|
31
32
|
h.merge({
|
32
33
|
:log_level => 'verbose',
|
34
|
+
:trace_limit => 10,
|
33
35
|
:hosts_file => 'sample.cfg',
|
34
36
|
:options_file => nil,
|
35
37
|
:type => 'pe',
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Beaker
|
2
|
+
class Platform < String
|
3
|
+
#supported platforms
|
4
|
+
PLATFORMS = /^(centos|fedora|debian|oracle|redhat|scientific|sles|ubuntu|windows|solaris|aix|el)\-.+\-.+$/
|
5
|
+
|
6
|
+
PLATFORM_VERSION_CODES =
|
7
|
+
{ :debian => { "wheezy" => "7",
|
8
|
+
"squeeze" => "6",
|
9
|
+
},
|
10
|
+
:ubuntu => { "trusty" => "1404",
|
11
|
+
"saucy" => "1310",
|
12
|
+
"raring" => "1304",
|
13
|
+
"quantal" => "1210",
|
14
|
+
"precise" => "1204",
|
15
|
+
},
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize(name)
|
19
|
+
if name !~ PLATFORMS
|
20
|
+
raise ArgumentError, "Unsupported platform name #{name}"
|
21
|
+
end
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def with_version_codename
|
26
|
+
name, version, extra = self.split('-', 3)
|
27
|
+
PLATFORM_VERSION_CODES.each_key do |platform|
|
28
|
+
if name =~ /#{platform}/
|
29
|
+
PLATFORM_VERSION_CODES[platform].each do |version_codename, version_number|
|
30
|
+
#remove '.' from version number
|
31
|
+
if version.delete('.') =~ /#{version_number}/
|
32
|
+
version = version_codename
|
33
|
+
break
|
34
|
+
end
|
35
|
+
end
|
36
|
+
break
|
37
|
+
end
|
38
|
+
end
|
39
|
+
[name, version, extra].join('-')
|
40
|
+
end
|
41
|
+
|
42
|
+
def with_version_number
|
43
|
+
name, version, extra = self.split('-', 3)
|
44
|
+
PLATFORM_VERSION_CODES.each_key do |platform|
|
45
|
+
if name =~ /#{platform}/
|
46
|
+
PLATFORM_VERSION_CODES[platform].each do |version_codename, version_number|
|
47
|
+
if version =~ /#{version_codename}/
|
48
|
+
version = version_number
|
49
|
+
break
|
50
|
+
end
|
51
|
+
end
|
52
|
+
break
|
53
|
+
end
|
54
|
+
end
|
55
|
+
[name, version, extra].join('-')
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -174,6 +174,7 @@ module Beaker
|
|
174
174
|
# Net::Scp always returns 0, so just set the return code to 0.
|
175
175
|
result.exit_code = 0
|
176
176
|
|
177
|
+
result.finalize!
|
177
178
|
return result
|
178
179
|
end
|
179
180
|
|
@@ -192,6 +193,7 @@ module Beaker
|
|
192
193
|
# Net::Scp always returns 0, so just set the return code to 0.
|
193
194
|
result.exit_code = 0
|
194
195
|
|
196
|
+
result.finalize!
|
195
197
|
result
|
196
198
|
end
|
197
199
|
end
|
@@ -2,6 +2,8 @@ module Beaker
|
|
2
2
|
module Utils
|
3
3
|
class NTPControl
|
4
4
|
NTPSERVER = 'pool.ntp.org'
|
5
|
+
SLEEPWAIT = 5
|
6
|
+
TRIES = 5
|
5
7
|
def initialize(options, hosts)
|
6
8
|
@options = options.dup
|
7
9
|
@hosts = hosts
|
@@ -12,31 +14,44 @@ module Beaker
|
|
12
14
|
@logger.notify "Update system time sync"
|
13
15
|
@logger.notify "run ntpdate against NTP pool systems"
|
14
16
|
@hosts.each do |host|
|
15
|
-
|
16
|
-
if host['platform'].include? 'solaris-10'
|
17
|
-
host.exec(Command.new("sleep 10 && ntpdate -w #{NTPSERVER}"))
|
18
|
-
elsif host['platform'].include? 'windows'
|
17
|
+
if host['platform'].include? 'windows'
|
19
18
|
# The exit code of 5 is for Windows 2008 systems where the w32tm /register command
|
20
19
|
# is not actually necessary.
|
21
20
|
host.exec(Command.new("w32tm /register"), :acceptable_exit_codes => [0,5])
|
22
21
|
host.exec(Command.new("net start w32time"), :acceptable_exit_codes => [0,2])
|
23
22
|
host.exec(Command.new("w32tm /config /manualpeerlist:#{NTPSERVER} /syncfromflags:manual /update"))
|
24
23
|
host.exec(Command.new("w32tm /resync"))
|
24
|
+
@logger.notify "NTP date succeeded on #{host}"
|
25
25
|
else
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
case
|
27
|
+
when host['platform'] =~ /solaris-10/
|
28
|
+
ntp_command = "sleep 10 && ntpdate -w #{NTPSERVER}"
|
29
|
+
when host['platform'] =~ /sles-/
|
30
|
+
ntp_command = "sntp #{NTPSERVER}"
|
31
|
+
else
|
32
|
+
ntp_command = "ntpdate -t 20 #{NTPSERVER}"
|
33
|
+
end
|
34
|
+
success=false
|
35
|
+
try = 0
|
36
|
+
until try >= TRIES do
|
37
|
+
try += 1
|
38
|
+
if host.exec(Command.new(ntp_command), :acceptable_exit_codes => (0..255)).exit_code == 0
|
39
|
+
success=true
|
40
|
+
break
|
32
41
|
end
|
42
|
+
sleep SLEEPWAIT
|
43
|
+
end
|
44
|
+
if success
|
45
|
+
@logger.notify "NTP date succeeded on #{host} after #{try} tries"
|
46
|
+
else
|
47
|
+
raise "NTP date was not successful after #{try} tries"
|
33
48
|
end
|
34
|
-
@logger.notify "NTP date succeeded after #{count} tries"
|
35
49
|
end
|
36
50
|
end
|
37
51
|
rescue => e
|
38
52
|
report_and_raise(@logger, e, "timesync (--ntp)")
|
39
53
|
end
|
54
|
+
|
40
55
|
end
|
41
56
|
end
|
42
57
|
end
|