train 3.2.14 → 3.2.20
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.
- checksums.yaml +4 -4
- metadata +29 -149
- data/LICENSE +0 -201
- data/lib/train.rb +0 -193
- data/lib/train/errors.rb +0 -44
- data/lib/train/extras.rb +0 -11
- data/lib/train/extras/command_wrapper.rb +0 -201
- data/lib/train/extras/stat.rb +0 -136
- data/lib/train/file.rb +0 -212
- data/lib/train/file/local.rb +0 -82
- data/lib/train/file/local/unix.rb +0 -96
- data/lib/train/file/local/windows.rb +0 -68
- data/lib/train/file/remote.rb +0 -40
- data/lib/train/file/remote/aix.rb +0 -29
- data/lib/train/file/remote/linux.rb +0 -21
- data/lib/train/file/remote/qnx.rb +0 -41
- data/lib/train/file/remote/unix.rb +0 -110
- data/lib/train/file/remote/windows.rb +0 -110
- data/lib/train/globals.rb +0 -5
- data/lib/train/options.rb +0 -81
- data/lib/train/platforms.rb +0 -102
- data/lib/train/platforms/common.rb +0 -34
- data/lib/train/platforms/detect.rb +0 -12
- data/lib/train/platforms/detect/helpers/os_common.rb +0 -160
- data/lib/train/platforms/detect/helpers/os_linux.rb +0 -80
- data/lib/train/platforms/detect/helpers/os_windows.rb +0 -142
- data/lib/train/platforms/detect/scanner.rb +0 -85
- data/lib/train/platforms/detect/specifications/api.rb +0 -20
- data/lib/train/platforms/detect/specifications/os.rb +0 -629
- data/lib/train/platforms/detect/uuid.rb +0 -32
- data/lib/train/platforms/family.rb +0 -31
- data/lib/train/platforms/platform.rb +0 -109
- data/lib/train/plugin_test_helper.rb +0 -51
- data/lib/train/plugins.rb +0 -40
- data/lib/train/plugins/base_connection.rb +0 -198
- data/lib/train/plugins/transport.rb +0 -49
- data/lib/train/transports/cisco_ios_connection.rb +0 -133
- data/lib/train/transports/local.rb +0 -240
- data/lib/train/transports/mock.rb +0 -183
- data/lib/train/transports/ssh.rb +0 -271
- data/lib/train/transports/ssh_connection.rb +0 -342
- data/lib/train/version.rb +0 -7
data/lib/train/extras/stat.rb
DELETED
@@ -1,136 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
# author: Dominik Richter
|
3
|
-
# author: Christoph Hartmann
|
4
|
-
module Train::Extras
|
5
|
-
class Stat
|
6
|
-
TYPES = {
|
7
|
-
socket: 00140000,
|
8
|
-
symlink: 00120000,
|
9
|
-
file: 00100000,
|
10
|
-
block_device: 00060000,
|
11
|
-
directory: 00040000,
|
12
|
-
character_device: 00020000,
|
13
|
-
pipe: 00010000,
|
14
|
-
}.freeze
|
15
|
-
|
16
|
-
def self.find_type(mode)
|
17
|
-
res = TYPES.find { |_, mask| mask & mode == mask }
|
18
|
-
res.nil? ? :unknown : res[0]
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.stat(shell_escaped_path, backend, follow_symlink)
|
22
|
-
# use perl scripts for aix, solaris 10 and hpux
|
23
|
-
if backend.os.aix? || (backend.os.solaris? && backend.os[:release].to_i < 11) || backend.os.hpux?
|
24
|
-
return aix_stat(shell_escaped_path, backend, follow_symlink)
|
25
|
-
end
|
26
|
-
return bsd_stat(shell_escaped_path, backend, follow_symlink) if backend.os.bsd?
|
27
|
-
# linux,solaris 11 and esx will use standard linux stats
|
28
|
-
return linux_stat(shell_escaped_path, backend, follow_symlink) if backend.os.unix? || backend.os.esx?
|
29
|
-
|
30
|
-
# all other cases we don't handle
|
31
|
-
# TODO: print an error if we get here, as it shouldn't be invoked
|
32
|
-
# on non-unix
|
33
|
-
{}
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.linux_stat(shell_escaped_path, backend, follow_symlink)
|
37
|
-
lstat = follow_symlink ? " -L" : ""
|
38
|
-
format = (backend.os.esx? || backend.os[:name] == "alpine" || backend.os[:name] == "yocto") ? "-c" : "--printf"
|
39
|
-
res = backend.run_command("stat#{lstat} #{shell_escaped_path} 2>/dev/null #{format} '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'")
|
40
|
-
# ignore the exit_code: it is != 0 if selinux labels are not supported
|
41
|
-
# on the system.
|
42
|
-
|
43
|
-
fields = res.stdout.split("\n")
|
44
|
-
return {} if fields.length != 9
|
45
|
-
|
46
|
-
tmask = fields[1].to_i(16)
|
47
|
-
selinux = fields[8]
|
48
|
-
## selinux security context string not available on esxi
|
49
|
-
selinux = nil if (selinux == "?") || (selinux == "(null)") || (selinux == "C")
|
50
|
-
{
|
51
|
-
type: find_type(tmask),
|
52
|
-
mode: tmask & 07777,
|
53
|
-
owner: fields[2],
|
54
|
-
uid: fields[3].to_i,
|
55
|
-
group: fields[4],
|
56
|
-
gid: fields[5].to_i,
|
57
|
-
mtime: fields[7].to_i,
|
58
|
-
size: fields[0].to_i,
|
59
|
-
selinux_label: selinux,
|
60
|
-
}
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.bsd_stat(shell_escaped_path, backend, follow_symlink)
|
64
|
-
# From stat man page on FreeBSD:
|
65
|
-
# z The size of file in bytes (st_size).
|
66
|
-
# p File type and permissions (st_mode).
|
67
|
-
# u, g User ID and group ID of file's owner (st_uid, st_gid).
|
68
|
-
# a, m, c, B
|
69
|
-
# The time file was last accessed or modified, or when the
|
70
|
-
# inode was last changed, or the birth time of the inode
|
71
|
-
# (st_atime, st_mtime, st_ctime, st_birthtime).
|
72
|
-
#
|
73
|
-
# The special output specifier S may be used to indicate that the
|
74
|
-
# output, if applicable, should be in string format. May be used
|
75
|
-
# in combination with:
|
76
|
-
# ...
|
77
|
-
# gu Display group or user name.
|
78
|
-
lstat = follow_symlink ? " -L" : ""
|
79
|
-
res = backend.run_command(
|
80
|
-
"stat#{lstat} -f '%z\n%p\n%Su\n%u\n%Sg\n%g\n%a\n%m' "\
|
81
|
-
"#{shell_escaped_path}"
|
82
|
-
)
|
83
|
-
|
84
|
-
return {} if res.exit_status != 0
|
85
|
-
|
86
|
-
fields = res.stdout.split("\n")
|
87
|
-
return {} if fields.length != 8
|
88
|
-
|
89
|
-
tmask = fields[1].to_i(8)
|
90
|
-
|
91
|
-
{
|
92
|
-
type: find_type(tmask),
|
93
|
-
mode: tmask & 07777,
|
94
|
-
owner: fields[2],
|
95
|
-
uid: fields[3].to_i,
|
96
|
-
group: fields[4],
|
97
|
-
gid: fields[5].to_i,
|
98
|
-
mtime: fields[7].to_i,
|
99
|
-
size: fields[0].to_i,
|
100
|
-
selinux_label: fields[8],
|
101
|
-
}
|
102
|
-
end
|
103
|
-
|
104
|
-
def self.aix_stat(shell_escaped_path, backend, follow_symlink)
|
105
|
-
# Perl here b/c it is default on AIX
|
106
|
-
lstat = follow_symlink ? "lstat" : "stat"
|
107
|
-
stat_cmd = <<-EOP
|
108
|
-
perl -e '
|
109
|
-
@a = #{lstat}(shift) or exit 2;
|
110
|
-
$u = getpwuid($a[4]);
|
111
|
-
$g = getgrgid($a[5]);
|
112
|
-
printf("0%o\\n%s\\n%d\\n%s\\n%d\\n%d\\n%d\\n", $a[2], $u, $a[4], $g, $a[5], $a[9], $a[7])
|
113
|
-
' #{shell_escaped_path}
|
114
|
-
EOP
|
115
|
-
|
116
|
-
res = backend.run_command(stat_cmd)
|
117
|
-
return {} if res.exit_status != 0
|
118
|
-
|
119
|
-
fields = res.stdout.split("\n")
|
120
|
-
return {} if fields.length != 7
|
121
|
-
|
122
|
-
tmask = fields[0].to_i(8)
|
123
|
-
{
|
124
|
-
type: find_type(tmask),
|
125
|
-
mode: tmask & 07777,
|
126
|
-
owner: fields[1],
|
127
|
-
uid: fields[2].to_i,
|
128
|
-
group: fields[3],
|
129
|
-
gid: fields[4].to_i,
|
130
|
-
mtime: fields[5].to_i,
|
131
|
-
size: fields[6].to_i,
|
132
|
-
selinux_label: nil,
|
133
|
-
}
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
data/lib/train/file.rb
DELETED
@@ -1,212 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
#
|
3
|
-
# author: Christoph Hartmann
|
4
|
-
# author: Dominik Richter
|
5
|
-
|
6
|
-
require_relative "file/local"
|
7
|
-
require_relative "file/remote"
|
8
|
-
require_relative "extras/stat"
|
9
|
-
|
10
|
-
module Train
|
11
|
-
class File # rubocop:disable Metrics/ClassLength
|
12
|
-
def initialize(backend, path, follow_symlink = true)
|
13
|
-
@backend = backend
|
14
|
-
@path = path || ""
|
15
|
-
@follow_symlink = follow_symlink
|
16
|
-
|
17
|
-
sanitize_filename(path)
|
18
|
-
end
|
19
|
-
|
20
|
-
# This method gets override by particular os class.
|
21
|
-
def sanitize_filename(_path)
|
22
|
-
nil
|
23
|
-
end
|
24
|
-
|
25
|
-
# interface methods: these fields should be implemented by every
|
26
|
-
# backend File
|
27
|
-
DATA_FIELDS = %w{
|
28
|
-
exist? mode owner group uid gid content mtime size selinux_label path
|
29
|
-
}.freeze
|
30
|
-
|
31
|
-
DATA_FIELDS.each do |m|
|
32
|
-
next if m == "path"
|
33
|
-
|
34
|
-
define_method m do
|
35
|
-
raise NotImplementedError, "File must implement the #{m}() method."
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def to_json
|
40
|
-
res = Hash[DATA_FIELDS.map { |x| [x, method(x).call] }]
|
41
|
-
# additional fields provided as input
|
42
|
-
res["type"] = type
|
43
|
-
res["follow_symlink"] = @follow_symlink
|
44
|
-
res
|
45
|
-
end
|
46
|
-
|
47
|
-
def type
|
48
|
-
:unknown
|
49
|
-
end
|
50
|
-
|
51
|
-
def source
|
52
|
-
if @follow_symlink
|
53
|
-
self.class.new(@backend, @path, false)
|
54
|
-
else
|
55
|
-
self
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def source_path
|
60
|
-
@path
|
61
|
-
end
|
62
|
-
|
63
|
-
# product_version is primarily used by Windows operating systems only and will be overwritten
|
64
|
-
# in Windows-related classes. Since this field is returned for all file objects, the acceptable
|
65
|
-
# default value is nil
|
66
|
-
def product_version
|
67
|
-
nil
|
68
|
-
end
|
69
|
-
|
70
|
-
# file_version is primarily used by Windows operating systems only and will be overwritten
|
71
|
-
# in Windows-related classes. Since this field is returned for all file objects, the acceptable
|
72
|
-
# default value is nil
|
73
|
-
def file_version
|
74
|
-
nil
|
75
|
-
end
|
76
|
-
|
77
|
-
def version?(version)
|
78
|
-
(product_version == version) ||
|
79
|
-
(file_version == version)
|
80
|
-
end
|
81
|
-
|
82
|
-
def block_device?
|
83
|
-
type.to_s == "block_device"
|
84
|
-
end
|
85
|
-
|
86
|
-
def character_device?
|
87
|
-
type.to_s == "character_device"
|
88
|
-
end
|
89
|
-
|
90
|
-
def pipe?
|
91
|
-
type.to_s == "pipe"
|
92
|
-
end
|
93
|
-
|
94
|
-
def file?
|
95
|
-
type.to_s == "file"
|
96
|
-
end
|
97
|
-
|
98
|
-
def socket?
|
99
|
-
type.to_s == "socket"
|
100
|
-
end
|
101
|
-
|
102
|
-
def directory?
|
103
|
-
type.to_s == "directory"
|
104
|
-
end
|
105
|
-
|
106
|
-
def symlink?
|
107
|
-
source.type.to_s == "symlink"
|
108
|
-
end
|
109
|
-
|
110
|
-
def owned_by?(sth)
|
111
|
-
owner == sth
|
112
|
-
end
|
113
|
-
|
114
|
-
def path
|
115
|
-
if symlink? && @follow_symlink
|
116
|
-
link_path
|
117
|
-
else
|
118
|
-
@path
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# if the OS-specific file class supports inquirying as to whether the
|
123
|
-
# file/device is mounted, the #mounted method should return a command
|
124
|
-
# object whose stdout will not be nil if indeed the device is mounted.
|
125
|
-
#
|
126
|
-
# if the OS-specific file class does not support checking for mount
|
127
|
-
# status, the method should not be implemented and this method will
|
128
|
-
# return false.
|
129
|
-
def mounted?
|
130
|
-
return false unless respond_to?(:mounted)
|
131
|
-
|
132
|
-
!mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty?
|
133
|
-
end
|
134
|
-
|
135
|
-
def md5sum
|
136
|
-
# Skip processing rest of method if fallback method is selected
|
137
|
-
return perform_checksum_ruby(:md5) if defined?(@ruby_checksum_fallback)
|
138
|
-
|
139
|
-
checksum = if @backend.os.family == "windows"
|
140
|
-
perform_checksum_windows(:md5)
|
141
|
-
else
|
142
|
-
@md5_command ||= case @backend.os.family
|
143
|
-
when "darwin"
|
144
|
-
"md5 -r"
|
145
|
-
when "solaris"
|
146
|
-
"digest -a md5"
|
147
|
-
else
|
148
|
-
"md5sum"
|
149
|
-
end
|
150
|
-
|
151
|
-
perform_checksum_unix(@md5_command)
|
152
|
-
end
|
153
|
-
|
154
|
-
checksum || perform_checksum_ruby(:md5)
|
155
|
-
end
|
156
|
-
|
157
|
-
def sha256sum
|
158
|
-
# Skip processing rest of method if fallback method is selected
|
159
|
-
return perform_checksum_ruby(:sha256) if defined?(@ruby_checksum_fallback)
|
160
|
-
|
161
|
-
checksum = if @backend.os.family == "windows"
|
162
|
-
perform_checksum_windows(:sha256)
|
163
|
-
else
|
164
|
-
@sha256_command ||= case @backend.os.family
|
165
|
-
when "darwin", "hpux", "qnx"
|
166
|
-
"shasum -a 256"
|
167
|
-
when "solaris"
|
168
|
-
"digest -a sha256"
|
169
|
-
else
|
170
|
-
"sha256sum"
|
171
|
-
end
|
172
|
-
|
173
|
-
perform_checksum_unix(@sha256_command)
|
174
|
-
end
|
175
|
-
|
176
|
-
checksum || perform_checksum_ruby(:sha256)
|
177
|
-
end
|
178
|
-
|
179
|
-
private
|
180
|
-
|
181
|
-
def perform_checksum_unix(cmd)
|
182
|
-
res = @backend.run_command("#{cmd} #{@path}")
|
183
|
-
res.stdout.split(" ").first if res.exit_status == 0
|
184
|
-
end
|
185
|
-
|
186
|
-
def perform_checksum_windows(method)
|
187
|
-
cmd = "CertUtil -hashfile #{@path} #{method.to_s.upcase}"
|
188
|
-
res = @backend.run_command(cmd)
|
189
|
-
res.stdout.split("\r\n")[1].tr(" ", "") if res.exit_status == 0
|
190
|
-
end
|
191
|
-
|
192
|
-
# This pulls the content of the file to the machine running Train and uses
|
193
|
-
# Digest to perform the checksum. This is less efficient than using remote
|
194
|
-
# system binaries and can lead to incorrect results due to encoding.
|
195
|
-
def perform_checksum_ruby(method)
|
196
|
-
# This is used to skip attempting other checksum methods. If this is set
|
197
|
-
# then we know all other methods have failed.
|
198
|
-
@ruby_checksum_fallback = true
|
199
|
-
case method
|
200
|
-
when :md5
|
201
|
-
res = Digest::MD5.new
|
202
|
-
when :sha256
|
203
|
-
res = Digest::SHA256.new
|
204
|
-
end
|
205
|
-
|
206
|
-
res.update(content)
|
207
|
-
res.hexdigest
|
208
|
-
rescue TypeError => _
|
209
|
-
nil
|
210
|
-
end
|
211
|
-
end
|
212
|
-
end
|
data/lib/train/file/local.rb
DELETED
@@ -1,82 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
module Train
|
4
|
-
class File
|
5
|
-
class Local < Train::File
|
6
|
-
%w{
|
7
|
-
exist? file? socket? directory? symlink? pipe? size basename
|
8
|
-
}.each do |m|
|
9
|
-
define_method m.to_sym do
|
10
|
-
::File.method(m.to_sym).call(@path)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
def content
|
15
|
-
@content ||= ::File.read(@path, encoding: "UTF-8")
|
16
|
-
rescue StandardError => _
|
17
|
-
nil
|
18
|
-
end
|
19
|
-
|
20
|
-
def link_path
|
21
|
-
return nil unless symlink?
|
22
|
-
|
23
|
-
begin
|
24
|
-
@link_path ||= ::File.realpath(@path)
|
25
|
-
rescue Errno::ELOOP => _
|
26
|
-
# Leave it blank on symbolic loop, same as readlink
|
27
|
-
@link_path = ""
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def shallow_link_path
|
32
|
-
return nil unless symlink?
|
33
|
-
|
34
|
-
@link_path ||= ::File.readlink(@path)
|
35
|
-
end
|
36
|
-
|
37
|
-
def block_device?
|
38
|
-
::File.blockdev?(@path)
|
39
|
-
end
|
40
|
-
|
41
|
-
def character_device?
|
42
|
-
::File.chardev?(@path)
|
43
|
-
end
|
44
|
-
|
45
|
-
def type
|
46
|
-
case ::File.ftype(@path)
|
47
|
-
when "blockSpecial"
|
48
|
-
:block_device
|
49
|
-
when "characterSpecial"
|
50
|
-
:character_device
|
51
|
-
when "link"
|
52
|
-
:symlink
|
53
|
-
when "fifo"
|
54
|
-
:pipe
|
55
|
-
else
|
56
|
-
::File.ftype(@path).to_sym
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
%w{
|
61
|
-
mode owner group uid gid mtime selinux_label
|
62
|
-
}.each do |field|
|
63
|
-
define_method field.to_sym do
|
64
|
-
stat[field.to_sym]
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def mode?(sth)
|
69
|
-
mode == sth
|
70
|
-
end
|
71
|
-
|
72
|
-
def linked_to?(dst)
|
73
|
-
link_path == dst
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
# subclass requires are loaded after Train::File::Local is defined
|
80
|
-
# to avoid superclass mismatch errors
|
81
|
-
require_relative "local/unix"
|
82
|
-
require_relative "local/windows"
|
@@ -1,96 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
require "shellwords"
|
4
|
-
require_relative "../../extras/stat"
|
5
|
-
|
6
|
-
module Train
|
7
|
-
class File
|
8
|
-
class Local
|
9
|
-
class Unix < Train::File::Local
|
10
|
-
def sanitize_filename(path)
|
11
|
-
@spath = Shellwords.escape(path) || @path
|
12
|
-
end
|
13
|
-
|
14
|
-
def stat
|
15
|
-
return @stat if defined?(@stat)
|
16
|
-
|
17
|
-
begin
|
18
|
-
file_stat =
|
19
|
-
if @follow_symlink
|
20
|
-
::File.stat(@path)
|
21
|
-
else
|
22
|
-
::File.lstat(@path)
|
23
|
-
end
|
24
|
-
rescue StandardError => _err
|
25
|
-
return @stat = {}
|
26
|
-
end
|
27
|
-
|
28
|
-
@stat = {
|
29
|
-
type: Train::Extras::Stat.find_type(file_stat.mode),
|
30
|
-
mode: file_stat.mode & 07777,
|
31
|
-
mtime: file_stat.mtime.to_i,
|
32
|
-
size: file_stat.size,
|
33
|
-
owner: pw_username(file_stat.uid),
|
34
|
-
uid: file_stat.uid,
|
35
|
-
group: pw_groupname(file_stat.gid),
|
36
|
-
gid: file_stat.gid,
|
37
|
-
}
|
38
|
-
|
39
|
-
lstat = @follow_symlink ? " -L" : ""
|
40
|
-
res = @backend.run_command("stat#{lstat} #{@spath} 2>/dev/null --printf '%C'")
|
41
|
-
if res.exit_status == 0 && !res.stdout.empty? && res.stdout != "?"
|
42
|
-
@stat[:selinux_label] = res.stdout.strip
|
43
|
-
end
|
44
|
-
|
45
|
-
@stat
|
46
|
-
end
|
47
|
-
|
48
|
-
def mounted
|
49
|
-
@mounted ||=
|
50
|
-
@backend.run_command("mount | grep -- ' on #{@path} '")
|
51
|
-
end
|
52
|
-
|
53
|
-
def grouped_into?(sth)
|
54
|
-
group == sth
|
55
|
-
end
|
56
|
-
|
57
|
-
def unix_mode_mask(owner, type)
|
58
|
-
o = UNIX_MODE_OWNERS[owner.to_sym]
|
59
|
-
return nil if o.nil?
|
60
|
-
|
61
|
-
t = UNIX_MODE_TYPES[type.to_sym]
|
62
|
-
return nil if t.nil?
|
63
|
-
|
64
|
-
t & o
|
65
|
-
end
|
66
|
-
|
67
|
-
private
|
68
|
-
|
69
|
-
def pw_username(uid)
|
70
|
-
Etc.getpwuid(uid).name
|
71
|
-
rescue ArgumentError => _
|
72
|
-
nil
|
73
|
-
end
|
74
|
-
|
75
|
-
def pw_groupname(gid)
|
76
|
-
Etc.getgrgid(gid).name
|
77
|
-
rescue ArgumentError => _
|
78
|
-
nil
|
79
|
-
end
|
80
|
-
|
81
|
-
UNIX_MODE_OWNERS = {
|
82
|
-
all: 00777,
|
83
|
-
owner: 00700,
|
84
|
-
group: 00070,
|
85
|
-
other: 00007,
|
86
|
-
}.freeze
|
87
|
-
|
88
|
-
UNIX_MODE_TYPES = {
|
89
|
-
r: 00444,
|
90
|
-
w: 00222,
|
91
|
-
x: 00111,
|
92
|
-
}.freeze
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|