nagios-promoo 1.1.0 → 1.2.0
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
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +29 -0
- data/.travis.yml +27 -0
- data/README.md +8 -4
- data/Rakefile +6 -1
- data/lib/nagios/promoo.rb +12 -6
- data/lib/nagios/promoo/appdb/master.rb +52 -25
- data/lib/nagios/promoo/appdb/probes/appliances_probe.rb +50 -37
- data/lib/nagios/promoo/appdb/probes/base_probe.rb +60 -40
- data/lib/nagios/promoo/appdb/probes/sizes_probe.rb +50 -37
- data/lib/nagios/promoo/appdb/probes/sync_probe.rb +179 -0
- data/lib/nagios/promoo/appdb/version.rb +1 -1
- data/lib/nagios/promoo/master.rb +37 -19
- data/lib/nagios/promoo/occi/master.rb +58 -27
- data/lib/nagios/promoo/occi/probes/base_probe.rb +37 -24
- data/lib/nagios/promoo/occi/probes/categories_probe.rb +88 -46
- data/lib/nagios/promoo/occi/probes/compute_probe.rb +249 -202
- data/lib/nagios/promoo/occi/probes/kinds_probe.rb +83 -52
- data/lib/nagios/promoo/occi/probes/mixins_probe.rb +62 -37
- data/lib/nagios/promoo/occi/version.rb +1 -1
- data/lib/nagios/promoo/opennebula/master.rb +55 -26
- data/lib/nagios/promoo/opennebula/probes/base_probe.rb +35 -12
- data/lib/nagios/promoo/opennebula/probes/virtual_machine_probe.rb +138 -100
- data/lib/nagios/promoo/opennebula/probes/xmlrpc_health_probe.rb +34 -21
- data/lib/nagios/promoo/opennebula/version.rb +1 -1
- data/lib/nagios/promoo/utils.rb +47 -25
- data/lib/nagios/promoo/version.rb +1 -1
- data/nagios-promoo.gemspec +5 -4
- metadata +9 -6
- data/lib/nagios/promoo/appdb/probes/vmcatcher_probe.rb +0 -118
@@ -0,0 +1,179 @@
|
|
1
|
+
# Internal deps
|
2
|
+
require File.join(File.dirname(__FILE__), 'base_probe')
|
3
|
+
|
4
|
+
module Nagios
|
5
|
+
module Promoo
|
6
|
+
module Appdb
|
7
|
+
module Probes
|
8
|
+
# Probe for checking appliance synchronization between sites and AppDB.
|
9
|
+
#
|
10
|
+
# @author Boris Parak <parak@cesnet.cz>
|
11
|
+
class SyncProbe < Nagios::Promoo::Appdb::Probes::BaseProbe
|
12
|
+
class << self
|
13
|
+
def description
|
14
|
+
[
|
15
|
+
'sync',
|
16
|
+
'Run a probe checking consistency between a published VO-wide ' \
|
17
|
+
'image list and appliances available at the site (via AppDB)'
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
def options
|
22
|
+
[
|
23
|
+
[
|
24
|
+
:vo,
|
25
|
+
{
|
26
|
+
type: :string,
|
27
|
+
required: true,
|
28
|
+
desc: 'Virtual Organization name (used to select the appropriate VO-wide image list)'
|
29
|
+
}
|
30
|
+
],
|
31
|
+
[
|
32
|
+
:token,
|
33
|
+
{
|
34
|
+
type: :string,
|
35
|
+
required: true,
|
36
|
+
desc: 'AppDB authentication token (used to access the VO-wide image list)'
|
37
|
+
}
|
38
|
+
],
|
39
|
+
[
|
40
|
+
:warning_after,
|
41
|
+
{
|
42
|
+
type: :numeric,
|
43
|
+
default: 24,
|
44
|
+
desc: 'A number of hours after list publication when missing or outdated appliances raise WARNING'
|
45
|
+
}
|
46
|
+
],
|
47
|
+
[
|
48
|
+
:critical_after,
|
49
|
+
{
|
50
|
+
type: :numeric,
|
51
|
+
default: 72,
|
52
|
+
desc: 'A number of hours after list publication when missing or outdated appliances raise CRITICAL'
|
53
|
+
}
|
54
|
+
]
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
def declaration
|
59
|
+
'sync'
|
60
|
+
end
|
61
|
+
|
62
|
+
def runnable?
|
63
|
+
true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
IMAGE_LIST_TEMPLATE = 'https://$$TOKEN$$:x-oauth-basic@vmcaster.appdb.egi.eu' \
|
68
|
+
'/store/vo/$$VO$$/image.list'.freeze
|
69
|
+
|
70
|
+
def run(_args = [])
|
71
|
+
@_results = { found: [], outdated: [], missing: [], expected: [] }
|
72
|
+
|
73
|
+
Timeout.timeout(options[:timeout]) { check_vmc_sync }
|
74
|
+
|
75
|
+
wrong = @_results[:missing] + @_results[:outdated]
|
76
|
+
if wrong.any?
|
77
|
+
if (@_last_update + options[:critical_after].hours) < Time.now
|
78
|
+
puts "SYNC CRITICAL - Appliance(s) #{wrong.inspect} missing " \
|
79
|
+
"or outdated in #{options[:vo].inspect} " \
|
80
|
+
"more than #{options[:critical_after]} hours after list publication [#{@_last_update}]"
|
81
|
+
exit 2
|
82
|
+
end
|
83
|
+
|
84
|
+
if (@_last_update + options[:warning_after].hours) < Time.now
|
85
|
+
puts "SYNC WARNING - Appliance(s) #{wrong.inspect} missing " \
|
86
|
+
"or outdated in #{options[:vo].inspect} " \
|
87
|
+
"more than #{options[:warning_after]} hours after list publication [#{@_last_update}]"
|
88
|
+
exit 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
puts "SYNC OK - All appliances registered in #{options[:vo].inspect} " \
|
93
|
+
"are available [#{@_results[:expected].count}]"
|
94
|
+
rescue => ex
|
95
|
+
puts "SYNC UNKNOWN - #{ex.message}"
|
96
|
+
puts ex.backtrace if options[:debug]
|
97
|
+
exit 3
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def check_vmc_sync
|
103
|
+
vo_list.each do |hv_image|
|
104
|
+
mpuri_versionless = versionless_mpuri(hv_image['ad:mpuri'])
|
105
|
+
@_results[:expected] << mpuri_versionless
|
106
|
+
|
107
|
+
matching = provider_appliances.detect { |appl| appl['mp_uri'] == mpuri_versionless }
|
108
|
+
unless matching
|
109
|
+
@_results[:missing] << mpuri_versionless
|
110
|
+
next
|
111
|
+
end
|
112
|
+
|
113
|
+
@_results[:outdated] << mpuri_versionless if hv_image['hv:version'] != matching['vmiversion']
|
114
|
+
@_results[:found] << mpuri_versionless
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def provider_appliances
|
119
|
+
return @_appliances if @_appliances
|
120
|
+
|
121
|
+
@_appliances = [appdb_provider['provider:image']].flatten.compact
|
122
|
+
@_appliances.keep_if { |appliance| appliance['voname'] == options[:vo] }
|
123
|
+
@_appliances.reject { |appliance| appliance['mp_uri'].blank? }
|
124
|
+
|
125
|
+
@_appliances.each do |appliance|
|
126
|
+
appliance['mp_uri'] = versionless_mpuri(appliance['mp_uri'])
|
127
|
+
end
|
128
|
+
|
129
|
+
@_appliances
|
130
|
+
end
|
131
|
+
|
132
|
+
def vo_list
|
133
|
+
return @_hv_images if @_hv_images
|
134
|
+
|
135
|
+
list = JSON.parse pkcs7_data
|
136
|
+
raise "AppDB image list #{list_url.inspect} is empty or malformed" unless list && list['hv:imagelist']
|
137
|
+
|
138
|
+
list = list['hv:imagelist']
|
139
|
+
unless DateTime.parse(list['dc:date:expires']) > Time.now
|
140
|
+
raise "AppDB image list #{list_url.inspect} has expired"
|
141
|
+
end
|
142
|
+
raise "AppDB image list #{list_url.inspect} doesn't contain images" unless list['hv:images']
|
143
|
+
@_last_update = DateTime.parse list['dc:date:created']
|
144
|
+
|
145
|
+
@_hv_images = list['hv:images'].collect { |im| im['hv:image'] }
|
146
|
+
@_hv_images.reject! { |im| im.blank? || im['ad:mpuri'].blank? }
|
147
|
+
@_hv_images
|
148
|
+
end
|
149
|
+
|
150
|
+
def pkcs7_data
|
151
|
+
content = OpenSSL::PKCS7.read_smime(retrieve_list)
|
152
|
+
content.data
|
153
|
+
end
|
154
|
+
|
155
|
+
def retrieve_list
|
156
|
+
response = HTTParty.get list_url
|
157
|
+
unless response.success?
|
158
|
+
raise 'Could not get a VO-wide image list' \
|
159
|
+
"from #{list_url.inspect} [#{response.code}]"
|
160
|
+
end
|
161
|
+
response.parsed_response
|
162
|
+
end
|
163
|
+
|
164
|
+
def list_url
|
165
|
+
IMAGE_LIST_TEMPLATE.gsub('$$TOKEN$$', options[:token]).gsub('$$VO$$', options[:vo])
|
166
|
+
end
|
167
|
+
|
168
|
+
def normalize_mpuri(mpuri)
|
169
|
+
mpuri.gsub(%r{/+$}, '')
|
170
|
+
end
|
171
|
+
|
172
|
+
def versionless_mpuri(mpuri)
|
173
|
+
normalize_mpuri(mpuri).gsub(/:\d+$/, '')
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/lib/nagios/promoo/master.rb
CHANGED
@@ -4,29 +4,47 @@
|
|
4
4
|
# Include available probe modules
|
5
5
|
Dir.glob(File.join(File.dirname(__FILE__), '*', 'master.rb')) { |mod| require mod.chomp('.rb') }
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
module Nagios
|
8
|
+
module Promoo
|
9
|
+
# Main class for `nagios-promoo`.
|
10
|
+
#
|
11
|
+
# @author Boris Parak <parak@cesnet.cz>
|
12
|
+
class Master < ::Thor
|
13
|
+
class_option :debug, type: :boolean, desc: 'Turn on debugging mode', default: false
|
14
|
+
class_option :ca_path,
|
15
|
+
type: :string,
|
16
|
+
desc: 'Path to a directory with CA certificates',
|
17
|
+
default: '/etc/grid-security/certificates'
|
18
|
+
class_option :ca_file, type: :string, desc: 'Path to a file with CA certificates'
|
19
|
+
class_option :insecure,
|
20
|
+
type: :boolean,
|
21
|
+
desc: 'Turn on insecure mode (without SSL client validation)',
|
22
|
+
default: false
|
23
|
+
class_option :timeout,
|
24
|
+
type: :numeric,
|
25
|
+
desc: 'Timeout for all internal connections and other processes (in seconds)',
|
26
|
+
default: 720
|
13
27
|
|
14
|
-
|
15
|
-
|
28
|
+
desc 'opennebula PROBE', 'Run the given probe for OpenNebula'
|
29
|
+
subcommand 'opennebula', Nagios::Promoo::Opennebula::Master
|
16
30
|
|
17
|
-
|
18
|
-
|
31
|
+
desc 'occi PROBE', 'Run the given probe for OCCI'
|
32
|
+
subcommand 'occi', Nagios::Promoo::Occi::Master
|
19
33
|
|
20
|
-
|
21
|
-
|
34
|
+
desc 'appdb PROBE', 'Run the given probe for AppDB'
|
35
|
+
subcommand 'appdb', Nagios::Promoo::Appdb::Master
|
22
36
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
37
|
+
desc 'version', 'Print PROMOO version'
|
38
|
+
def version
|
39
|
+
puts Nagios::Promoo::VERSION
|
40
|
+
end
|
27
41
|
|
28
|
-
|
29
|
-
|
30
|
-
|
42
|
+
class << self
|
43
|
+
# Force thor to exit with a non-zero return code on failure
|
44
|
+
def exit_on_failure?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
31
49
|
end
|
32
50
|
end
|
@@ -4,42 +4,73 @@ require 'occi-api'
|
|
4
4
|
# Internal deps
|
5
5
|
require File.join(File.dirname(__FILE__), 'version')
|
6
6
|
|
7
|
-
|
8
|
-
module
|
9
|
-
|
7
|
+
module Nagios
|
8
|
+
module Promoo
|
9
|
+
# Namespace for OCCI-related code.
|
10
|
+
#
|
11
|
+
# @author Boris Parak <parak@cesnet.cz>
|
12
|
+
module Occi
|
13
|
+
# Namespace for OCCI-related probes.
|
14
|
+
#
|
15
|
+
# @author Boris Parak <parak@cesnet.cz>
|
16
|
+
module Probes; end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
10
21
|
Dir.glob(File.join(File.dirname(__FILE__), 'probes', '*.rb')) { |probe| require probe.chomp('.rb') }
|
11
22
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
23
|
+
module Nagios
|
24
|
+
module Promoo
|
25
|
+
module Occi
|
26
|
+
# Master class for all OCCI probes.
|
27
|
+
#
|
28
|
+
# @author Boris Parak <parak@cesnet.cz>
|
29
|
+
class Master < ::Thor
|
30
|
+
class << self
|
31
|
+
# Hack to override the help message produced by Thor.
|
32
|
+
# https://github.com/wycats/thor/issues/261#issuecomment-16880836
|
33
|
+
def banner(command, _namespace = nil, _subcommand = nil)
|
34
|
+
"#{basename} occi #{command.usage}"
|
35
|
+
end
|
19
36
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
37
|
+
def available_probes
|
38
|
+
probes = Nagios::Promoo::Occi::Probes.constants.collect do |probe|
|
39
|
+
Nagios::Promoo::Occi::Probes.const_get(probe)
|
40
|
+
end
|
41
|
+
probes.select(&:runnable?)
|
42
|
+
end
|
43
|
+
end
|
24
44
|
|
25
|
-
|
26
|
-
|
27
|
-
|
45
|
+
class_option :endpoint, type: :string, desc: 'OCCI-enabled endpoint', default: 'http://localhost:3000/'
|
46
|
+
class_option :auth,
|
47
|
+
type: :string,
|
48
|
+
desc: 'Authentication mechanism',
|
49
|
+
enum: %w[x509-voms],
|
50
|
+
default: 'x509-voms'
|
51
|
+
class_option :token,
|
52
|
+
type: :string,
|
53
|
+
desc: 'Authentication token',
|
54
|
+
default: "file:///tmp/x509up_u#{`id -u`.strip}"
|
28
55
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
56
|
+
available_probes.each do |probe|
|
57
|
+
desc(*probe.description)
|
58
|
+
probe.options.each do |opt|
|
59
|
+
option opt.first, opt.last
|
60
|
+
end
|
61
|
+
|
62
|
+
class_eval %^
|
35
63
|
def #{probe.declaration}(*args)
|
36
64
|
#{probe}.new(options).run(args)
|
37
65
|
end
|
38
66
|
^
|
39
|
-
|
67
|
+
end
|
40
68
|
|
41
|
-
|
42
|
-
|
43
|
-
|
69
|
+
desc 'version', 'Print version of the OCCI probe set'
|
70
|
+
def version
|
71
|
+
puts Nagios::Promoo::Occi::VERSION
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
44
75
|
end
|
45
76
|
end
|
@@ -1,29 +1,42 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module Nagios
|
2
|
+
module Promoo
|
3
|
+
module Occi
|
4
|
+
module Probes
|
5
|
+
# Base probe for all OCCI-related probes.
|
6
|
+
#
|
7
|
+
# @author Boris Parak <parak@cesnet.cz>
|
8
|
+
class BaseProbe
|
9
|
+
class << self
|
10
|
+
def runnable?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
5
14
|
|
6
|
-
|
15
|
+
attr_reader :options
|
7
16
|
|
8
|
-
|
9
|
-
|
10
|
-
|
17
|
+
def initialize(options)
|
18
|
+
@options = options
|
19
|
+
end
|
11
20
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
def client
|
22
|
+
@_client ||= ::Occi::Api::Client::ClientHttp.new(
|
23
|
+
endpoint: options[:endpoint],
|
24
|
+
auth: {
|
25
|
+
type: options[:auth].gsub('-voms', ''),
|
26
|
+
user_cert: options[:token].gsub('file://', ''),
|
27
|
+
user_cert_password: nil,
|
28
|
+
ca_path: options[:ca_path],
|
29
|
+
voms: options[:auth] == 'x509-voms' ? true : false
|
30
|
+
},
|
31
|
+
log: {
|
32
|
+
level: options[:debug] ? ::Occi::Api::Log::DEBUG : ::Occi::Api::Log::ERROR,
|
33
|
+
logger: nil,
|
34
|
+
out: '/dev/null'
|
35
|
+
}
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
28
41
|
end
|
29
42
|
end
|
@@ -3,58 +3,100 @@ require File.join(File.dirname(__FILE__), 'base_probe')
|
|
3
3
|
require File.join(File.dirname(__FILE__), 'kinds_probe')
|
4
4
|
require File.join(File.dirname(__FILE__), 'mixins_probe')
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
module Nagios
|
7
|
+
module Promoo
|
8
|
+
module Occi
|
9
|
+
module Probes
|
10
|
+
# Probe for checking OCCI categories declared by endpoints.
|
11
|
+
#
|
12
|
+
# @author Boris Parak <parak@cesnet.cz>
|
13
|
+
class CategoriesProbe < Nagios::Promoo::Occi::Probes::BaseProbe
|
14
|
+
class << self
|
15
|
+
def description
|
16
|
+
['categories', 'Run a probe checking for mandatory OCCI category definitions']
|
17
|
+
end
|
11
18
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
def options
|
20
|
+
[
|
21
|
+
[
|
22
|
+
:optional,
|
23
|
+
{
|
24
|
+
type: :array, default: [],
|
25
|
+
desc: 'Identifiers of optional categories (optional by force)'
|
26
|
+
}
|
27
|
+
],
|
28
|
+
[
|
29
|
+
:check_location,
|
30
|
+
{
|
31
|
+
type: :boolean, default: false,
|
32
|
+
desc: 'Verify declared REST locations for INFRA resources'
|
33
|
+
}
|
34
|
+
]
|
35
|
+
]
|
36
|
+
end
|
18
37
|
|
19
|
-
|
20
|
-
|
21
|
-
|
38
|
+
def declaration
|
39
|
+
'categories'
|
40
|
+
end
|
22
41
|
|
23
|
-
|
24
|
-
|
42
|
+
def runnable?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
25
46
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
Timeout::timeout(options[:timeout]) do
|
31
|
-
categories.each do |cat|
|
32
|
-
fail "#{cat.inspect} is missing" unless client.model.get_by_id(cat, true)
|
33
|
-
next unless options[:check_location] && Nagios::Promoo::Occi::Probes::KindsProbe::INFRA_KINDS.include?(cat)
|
34
|
-
|
35
|
-
# Make sure declared locations are actually available as REST
|
36
|
-
# endpoints. Failure will raise an exception, no need to do
|
37
|
-
# anything here. To keep requirements reasonable, only INFRA
|
38
|
-
# kinds are considered relevant for this part of the check.
|
39
|
-
begin
|
40
|
-
client.list(cat)
|
41
|
-
rescue => err
|
42
|
-
fail "Failed to verify declared REST location for #{cat.inspect} (#{err.message})"
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
47
|
+
def run(_args = [])
|
48
|
+
categories = all_categories
|
49
|
+
categories -= options[:optional] if options[:optional]
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
Timeout.timeout(options[:timeout]) do
|
52
|
+
categories.each do |cat|
|
53
|
+
raise "#{cat.inspect} is missing" unless client.model.get_by_id(cat, true)
|
54
|
+
next unless options[:check_location] && infra_kinds.include?(cat)
|
55
|
+
|
56
|
+
# Make sure declared locations are actually available as REST
|
57
|
+
# endpoints. Failure will raise an exception, no need to do
|
58
|
+
# anything here. To keep requirements reasonable, only INFRA
|
59
|
+
# kinds are considered relevant for this part of the check.
|
60
|
+
begin
|
61
|
+
client.list(cat)
|
62
|
+
rescue => ex
|
63
|
+
raise "Failed to verify declared REST location for #{cat.inspect} (#{ex.message})"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
53
67
|
|
54
|
-
|
68
|
+
puts 'CATEGORIES OK - All specified OCCI categories were found'
|
69
|
+
rescue => ex
|
70
|
+
puts "CATEGORIES CRITICAL - #{ex.message}"
|
71
|
+
puts ex.backtrace if options[:debug]
|
72
|
+
exit 2
|
73
|
+
end
|
55
74
|
|
56
|
-
|
57
|
-
|
58
|
-
|
75
|
+
private
|
76
|
+
|
77
|
+
def core_kinds
|
78
|
+
Nagios::Promoo::Occi::Probes::KindsProbe::CORE_KINDS
|
79
|
+
end
|
80
|
+
|
81
|
+
def infra_kinds
|
82
|
+
Nagios::Promoo::Occi::Probes::KindsProbe::INFRA_KINDS
|
83
|
+
end
|
84
|
+
|
85
|
+
def infra_mixins
|
86
|
+
Nagios::Promoo::Occi::Probes::MixinsProbe::INFRA_MIXINS
|
87
|
+
end
|
88
|
+
|
89
|
+
def context_mixins
|
90
|
+
Nagios::Promoo::Occi::Probes::MixinsProbe::CONTEXT_MIXINS
|
91
|
+
end
|
92
|
+
|
93
|
+
def all_categories
|
94
|
+
%i[core_kinds infra_kinds infra_mixins context_mixins].reduce([]) do |memo, elm|
|
95
|
+
memo.concat send(elm)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
59
101
|
end
|
60
102
|
end
|