icinga2 0.9.0.1 → 0.9.2.1
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/README.md +51 -45
- data/doc/hosts.md +62 -21
- data/doc/services.md +215 -54
- data/doc/statistics.md +28 -10
- data/doc/usergroups.md +49 -11
- data/doc/users.md +64 -13
- data/examples/_blank.rb +72 -0
- data/examples/downtimes.rb +79 -0
- data/examples/hostgroups.rb +91 -0
- data/examples/hosts.rb +180 -0
- data/examples/informations.rb +95 -0
- data/examples/notifications.rb +109 -0
- data/examples/servicegroups.rb +102 -0
- data/examples/services.rb +202 -0
- data/examples/statistics.rb +137 -0
- data/examples/test.rb +32 -377
- data/examples/usergroups.rb +95 -0
- data/examples/users.rb +98 -0
- data/lib/icinga2.rb +4 -394
- data/lib/icinga2/client.rb +431 -0
- data/lib/icinga2/downtimes.rb +40 -20
- data/lib/icinga2/hostgroups.rb +38 -22
- data/lib/icinga2/hosts.rb +308 -92
- data/lib/icinga2/network.rb +211 -213
- data/lib/icinga2/notifications.rb +30 -28
- data/lib/icinga2/servicegroups.rb +43 -24
- data/lib/icinga2/services.rb +218 -130
- data/lib/icinga2/statistics.rb +14 -10
- data/lib/icinga2/tools.rb +1 -1
- data/lib/icinga2/usergroups.rb +12 -15
- data/lib/icinga2/users.rb +16 -17
- data/lib/icinga2/version.rb +1 -15
- data/lib/monkey_patches.rb +89 -0
- metadata +21 -8
data/lib/icinga2/network.rb
CHANGED
@@ -17,73 +17,47 @@ module Icinga2
|
|
17
17
|
#
|
18
18
|
# @return [Hash]
|
19
19
|
#
|
20
|
-
def
|
20
|
+
def api_data( params )
|
21
21
|
|
22
|
-
raise ArgumentError.new('
|
22
|
+
raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
|
23
23
|
raise ArgumentError.new('missing params') if( params.size.zero? )
|
24
24
|
|
25
25
|
url = params.dig(:url)
|
26
26
|
headers = params.dig(:headers)
|
27
27
|
options = params.dig(:options)
|
28
|
-
payload = params.dig(:payload)
|
28
|
+
payload = params.dig(:payload)
|
29
29
|
|
30
30
|
raise ArgumentError.new('Missing url') if( url.nil? )
|
31
31
|
raise ArgumentError.new('Missing headers') if( headers.nil? )
|
32
32
|
raise ArgumentError.new('Missing options') if( options.nil? )
|
33
|
-
raise ArgumentError.new('only Hash for payload are allowed') unless( payload.is_a?(Hash) )
|
34
33
|
|
35
34
|
rest_client = RestClient::Resource.new( URI.encode( url ), options )
|
36
35
|
|
37
|
-
|
38
|
-
|
36
|
+
if( payload )
|
37
|
+
raise ArgumentError.new('only Hash for payload are allowed') unless( payload.is_a?(Hash) )
|
38
|
+
headers['X-HTTP-Method-Override'] = 'GET'
|
39
|
+
method = 'POST'
|
40
|
+
else
|
41
|
+
headers['X-HTTP-Method-Override'] = 'GET'
|
42
|
+
method = 'GET'
|
43
|
+
end
|
39
44
|
|
40
45
|
begin
|
41
|
-
|
42
|
-
headers['X-HTTP-Method-Override'] = 'GET'
|
43
|
-
payload = JSON.generate(payload)
|
44
|
-
res = rest_client.post(payload, headers)
|
45
|
-
else
|
46
|
-
res = rest_client.get(headers)
|
47
|
-
end
|
48
|
-
|
49
|
-
rescue RestClient::Unauthorized => e
|
46
|
+
data = request( rest_client, method, headers, payload )
|
50
47
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
}
|
48
|
+
data = JSON.parse( data ) if( data.is_a?(String) )
|
49
|
+
data = data.deep_string_keys
|
50
|
+
data = data.dig('results') if( data.is_a?(Hash) )
|
55
51
|
|
56
|
-
|
52
|
+
return data
|
57
53
|
|
58
|
-
|
59
|
-
# $stderr.puts( message )
|
54
|
+
rescue => e
|
60
55
|
|
61
|
-
|
62
|
-
|
63
|
-
message: message
|
64
|
-
}
|
56
|
+
logger.error(e)
|
57
|
+
logger.error(e.backtrace.join("\n"))
|
65
58
|
|
66
|
-
|
67
|
-
|
68
|
-
if( retried < max_retries )
|
69
|
-
retried += 1
|
70
|
-
$stderr.puts(format("Cannot execute request against '%s': '%s' (retry %d / %d)", url, e, retried, max_retries))
|
71
|
-
sleep(2)
|
72
|
-
retry
|
73
|
-
else
|
74
|
-
|
75
|
-
message = format( "Maximum retries (%d) against '%s' reached. Giving up ...", max_retries, url )
|
76
|
-
# $stderr.puts( message )
|
77
|
-
|
78
|
-
return {
|
79
|
-
status: 500,
|
80
|
-
message: message
|
81
|
-
}
|
82
|
-
end
|
59
|
+
return nil
|
83
60
|
end
|
84
|
-
|
85
|
-
body = res.body
|
86
|
-
JSON.parse(body)
|
87
61
|
end
|
88
62
|
|
89
63
|
# static function for GET Requests without filters
|
@@ -98,9 +72,9 @@ module Icinga2
|
|
98
72
|
#
|
99
73
|
# @return [Hash]
|
100
74
|
#
|
101
|
-
def
|
75
|
+
def icinga_application_data( params )
|
102
76
|
|
103
|
-
raise ArgumentError.new('
|
77
|
+
raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
|
104
78
|
raise ArgumentError.new('missing params') if( params.size.zero? )
|
105
79
|
|
106
80
|
url = params.dig(:url)
|
@@ -111,13 +85,23 @@ module Icinga2
|
|
111
85
|
raise ArgumentError.new('Missing headers') if( headers.nil? )
|
112
86
|
raise ArgumentError.new('Missing options') if( options.nil? )
|
113
87
|
|
114
|
-
|
88
|
+
begin
|
89
|
+
|
90
|
+
data = api_data( url: url, headers: headers, options: options )
|
91
|
+
data = data.first if( data.is_a?(Array) )
|
92
|
+
|
93
|
+
data
|
115
94
|
|
116
|
-
|
95
|
+
return data.dig('status') unless( data.nil? )
|
117
96
|
|
118
|
-
|
97
|
+
rescue => e
|
98
|
+
|
99
|
+
logger.error(e)
|
100
|
+
logger.error(e.backtrace.join("\n"))
|
101
|
+
|
102
|
+
return nil
|
103
|
+
end
|
119
104
|
|
120
|
-
return results.first.dig('status') unless( results.nil? )
|
121
105
|
end
|
122
106
|
|
123
107
|
# static function for POST Requests
|
@@ -131,9 +115,9 @@ module Icinga2
|
|
131
115
|
#
|
132
116
|
# @return [Hash]
|
133
117
|
#
|
134
|
-
def
|
118
|
+
def post( params )
|
135
119
|
|
136
|
-
raise ArgumentError.new('
|
120
|
+
raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
|
137
121
|
raise ArgumentError.new('missing params') if( params.size.zero? )
|
138
122
|
|
139
123
|
url = params.dig(:url)
|
@@ -146,71 +130,25 @@ module Icinga2
|
|
146
130
|
raise ArgumentError.new('Missing options') if( options.nil? )
|
147
131
|
raise ArgumentError.new('only Hash for payload are allowed') unless( payload.is_a?(Hash) )
|
148
132
|
|
149
|
-
|
150
|
-
retried = 0
|
151
|
-
result = {}
|
152
|
-
|
133
|
+
rest_client = RestClient::Resource.new( URI.encode( url ), options )
|
153
134
|
headers['X-HTTP-Method-Override'] = 'POST'
|
154
135
|
|
155
|
-
rest_client = RestClient::Resource.new(
|
156
|
-
URI.encode( url ),
|
157
|
-
options
|
158
|
-
)
|
159
|
-
|
160
136
|
begin
|
137
|
+
data = request( rest_client, 'POST', headers, payload )
|
161
138
|
|
162
|
-
data =
|
163
|
-
|
164
|
-
|
165
|
-
)
|
166
|
-
|
167
|
-
data = JSON.parse( data )
|
168
|
-
|
169
|
-
results = data.dig('results').first
|
170
|
-
|
171
|
-
return { status: results.dig('code').to_i, name: results.dig('name'), message: results.dig('status') } unless( results.nil? )
|
172
|
-
|
173
|
-
rescue RestClient::ExceptionWithResponse => e
|
174
|
-
|
175
|
-
error = e.response ? e.response : nil
|
176
|
-
error = JSON.parse( error ) if error.is_a?( String )
|
139
|
+
data = JSON.parse( data ) if( data.is_a?(String) )
|
140
|
+
data = data.deep_string_keys
|
141
|
+
data = data.dig('results').first if( data.is_a?(Hash) )
|
177
142
|
|
178
|
-
|
143
|
+
return { 'code' => data.dig('code').to_i, 'name' => data.dig('name'), 'status' => data.dig('status') } unless( data.nil? )
|
179
144
|
|
180
|
-
|
181
|
-
return {
|
182
|
-
status: results.dig('code').to_i,
|
183
|
-
name: results.dig('name'),
|
184
|
-
message: results.dig('status'),
|
185
|
-
error: results.dig('errors')
|
186
|
-
}
|
187
|
-
else
|
188
|
-
return {
|
189
|
-
status: error.dig( 'error' ).to_i,
|
190
|
-
message: error.dig( 'status' )
|
191
|
-
}
|
192
|
-
end
|
145
|
+
rescue => e
|
193
146
|
|
194
|
-
|
147
|
+
logger.error(e)
|
148
|
+
logger.error(e.backtrace.join("\n"))
|
195
149
|
|
196
|
-
|
197
|
-
retried += 1
|
198
|
-
$stderr.puts(format("Cannot execute request against '%s': '%s' (retry %d / %d)", url, e, retried, max_retries))
|
199
|
-
sleep(2)
|
200
|
-
retry
|
201
|
-
else
|
202
|
-
|
203
|
-
message = format( "Maximum retries (%d) against '%s' reached. Giving up ...", max_retries, url )
|
204
|
-
$stderr.puts( message )
|
205
|
-
|
206
|
-
return {
|
207
|
-
status: 500,
|
208
|
-
message: message
|
209
|
-
}
|
210
|
-
end
|
150
|
+
return nil
|
211
151
|
end
|
212
|
-
|
213
|
-
result
|
214
152
|
end
|
215
153
|
|
216
154
|
# static function for PUT Requests
|
@@ -225,9 +163,9 @@ module Icinga2
|
|
225
163
|
#
|
226
164
|
# @return [Hash]
|
227
165
|
#
|
228
|
-
def
|
166
|
+
def put( params )
|
229
167
|
|
230
|
-
raise ArgumentError.new('
|
168
|
+
raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
|
231
169
|
raise ArgumentError.new('missing params') if( params.size.zero? )
|
232
170
|
|
233
171
|
url = params.dig(:url)
|
@@ -240,72 +178,31 @@ module Icinga2
|
|
240
178
|
raise ArgumentError.new('Missing options') if( options.nil? )
|
241
179
|
raise ArgumentError.new('only Hash for payload are allowed') unless( payload.is_a?(Hash) )
|
242
180
|
|
243
|
-
|
244
|
-
retried = 0
|
245
|
-
result = {}
|
246
|
-
|
181
|
+
rest_client = RestClient::Resource.new( URI.encode( url ), options )
|
247
182
|
headers['X-HTTP-Method-Override'] = 'PUT'
|
248
183
|
|
249
|
-
rest_client = RestClient::Resource.new(
|
250
|
-
URI.encode( url ),
|
251
|
-
options
|
252
|
-
)
|
253
|
-
|
254
184
|
begin
|
255
|
-
data = rest_client.put(
|
256
|
-
JSON.generate( payload ),
|
257
|
-
headers
|
258
|
-
)
|
259
|
-
data = JSON.parse( data )
|
260
|
-
results = data.dig('results').first
|
261
|
-
|
262
|
-
return { status: results.dig('code').to_i, message: results.dig('status') } unless( results.nil? )
|
263
185
|
|
264
|
-
|
186
|
+
data = request( rest_client, 'PUT', headers, payload )
|
187
|
+
data = JSON.parse( data ) if( data.is_a?(String) )
|
188
|
+
data = data.deep_string_keys
|
265
189
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
results = error.dig('results')
|
270
|
-
|
271
|
-
return { status: error.dig('error').to_i, message: error.dig('status'), error: results } if( results.nil? )
|
272
|
-
|
273
|
-
if( results.is_a?( Hash ) && results.count != 0 )
|
274
|
-
|
275
|
-
return {
|
276
|
-
status: results.dig('code').to_i,
|
277
|
-
name: results.dig('name'),
|
278
|
-
message: results.dig('status'),
|
279
|
-
error: results
|
280
|
-
}
|
190
|
+
if( data.is_a?(Hash) )
|
191
|
+
results = data.dig('results')
|
192
|
+
results = results.first if( results.is_a?(Array) )
|
281
193
|
else
|
282
|
-
|
283
|
-
status: results.first.dig('code').to_i,
|
284
|
-
message: format('%s (possible, object already exists)', results.first.dig('status') ),
|
285
|
-
error: results
|
286
|
-
}
|
194
|
+
results = data
|
287
195
|
end
|
288
196
|
|
289
|
-
|
197
|
+
return { 'code' => results.dig('code').to_i, 'name' => results.dig('name'), 'status' => results.dig('status') } unless( results.nil? )
|
290
198
|
|
291
|
-
|
292
|
-
retried += 1
|
293
|
-
$stderr.puts(format("Cannot execute request against '%s': '%s' (retry %d / %d)", url, e, retried, max_retries))
|
294
|
-
sleep(2)
|
295
|
-
retry
|
296
|
-
else
|
199
|
+
rescue => e
|
297
200
|
|
298
|
-
|
299
|
-
|
201
|
+
logger.error(e)
|
202
|
+
logger.error(e.backtrace.join("\n"))
|
300
203
|
|
301
|
-
|
302
|
-
status: 500,
|
303
|
-
message: message
|
304
|
-
}
|
305
|
-
end
|
204
|
+
return nil
|
306
205
|
end
|
307
|
-
|
308
|
-
result
|
309
206
|
end
|
310
207
|
|
311
208
|
# static function for DELETE Requests
|
@@ -318,9 +215,9 @@ module Icinga2
|
|
318
215
|
#
|
319
216
|
# @return [Hash]
|
320
217
|
#
|
321
|
-
def
|
218
|
+
def delete( params )
|
322
219
|
|
323
|
-
raise ArgumentError.new('
|
220
|
+
raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
|
324
221
|
raise ArgumentError.new('missing params') if( params.size.zero? )
|
325
222
|
|
326
223
|
url = params.dig(:url)
|
@@ -331,74 +228,175 @@ module Icinga2
|
|
331
228
|
raise ArgumentError.new('Missing headers') if( headers.nil? )
|
332
229
|
raise ArgumentError.new('Missing options') if( options.nil? )
|
333
230
|
|
231
|
+
rest_client = RestClient::Resource.new( URI.encode( url ), options )
|
232
|
+
headers['X-HTTP-Method-Override'] = 'DELETE'
|
233
|
+
|
234
|
+
begin
|
235
|
+
data = request( rest_client, 'DELETE', headers )
|
236
|
+
|
237
|
+
data = JSON.parse( data ) if( data.is_a?(String) )
|
238
|
+
data = data.deep_string_keys
|
239
|
+
|
240
|
+
if( data.is_a?(Hash) )
|
241
|
+
results = data.dig('results')
|
242
|
+
results = results.first if( results.is_a?(Array) )
|
243
|
+
else
|
244
|
+
results = data
|
245
|
+
end
|
246
|
+
|
247
|
+
return { 'code' => results.dig('code').to_i, 'name' => results.dig('name'), 'status' => results.dig('status') } unless( results.nil? )
|
248
|
+
|
249
|
+
rescue => e
|
250
|
+
logger.error(e)
|
251
|
+
logger.error(e.backtrace.join("\n"))
|
252
|
+
|
253
|
+
return nil
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
#
|
259
|
+
# internal functionfor the Rest-Client Request
|
260
|
+
#
|
261
|
+
def request( client, method, headers, data = {} )
|
262
|
+
|
263
|
+
# logger.debug( "request( #{client.to_s}, #{method}, #{headers}, #{options}, #{data} )" )
|
264
|
+
|
265
|
+
raise ArgumentError.new('client must be an RestClient::Resource') unless( client.is_a?(RestClient::Resource) )
|
266
|
+
raise ArgumentError.new('method must be an \'GET\', \'POST\', \'PUT\' or \'DELETE\'') unless( %w[GET POST PUT DELETE].include?(method) )
|
267
|
+
|
268
|
+
unless( data.nil? )
|
269
|
+
raise ArgumentError.new(format('data must be an Hash (%s)', data.class.to_s)) unless( data.is_a?(Hash) )
|
270
|
+
end
|
271
|
+
|
334
272
|
max_retries = 3
|
335
273
|
retried = 0
|
336
274
|
|
337
|
-
|
275
|
+
begin
|
338
276
|
|
339
|
-
|
277
|
+
case method.upcase
|
278
|
+
when 'GET'
|
279
|
+
response = client.get( headers )
|
280
|
+
when 'POST'
|
281
|
+
response = client.post( data.to_json, headers )
|
282
|
+
when 'PATCH'
|
283
|
+
response = client.patch( data, headers )
|
284
|
+
when 'PUT'
|
285
|
+
# response = @api_instance[endpoint].put( data, @headers )
|
286
|
+
client.put( data.to_json, headers ) do |response, req, _result|
|
287
|
+
|
288
|
+
@req = req
|
289
|
+
@response_raw = response
|
290
|
+
@response_body = response.body
|
291
|
+
@response_code = response.code.to_i
|
292
|
+
|
293
|
+
# logger.debug('----------------------------')
|
294
|
+
# logger.debug(@response_raw)
|
295
|
+
# logger.debug(@response_body)
|
296
|
+
# logger.debug(@response_code)
|
297
|
+
# logger.debug('----------------------------')
|
298
|
+
|
299
|
+
case response.code
|
300
|
+
when 200
|
301
|
+
return @response_body
|
302
|
+
when 400
|
303
|
+
raise RestClient::BadRequest
|
304
|
+
when 404
|
305
|
+
raise RestClient::NotFound
|
306
|
+
when 500
|
307
|
+
raise RestClient::InternalServerError
|
308
|
+
else
|
309
|
+
response.return
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
when 'DELETE'
|
314
|
+
response = client.delete( @headers )
|
315
|
+
else
|
316
|
+
@logger.error( "Error: #{__method__} is not a valid request method." )
|
317
|
+
return false
|
318
|
+
end
|
340
319
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
)
|
320
|
+
response_body = response.body
|
321
|
+
response_headers = response.headers
|
322
|
+
response_body = JSON.parse( response_body )
|
345
323
|
|
346
|
-
|
347
|
-
data = rest_client.get( headers )
|
324
|
+
return response_body
|
348
325
|
|
349
|
-
|
326
|
+
rescue RestClient::BadRequest
|
350
327
|
|
351
|
-
|
352
|
-
results = data.dig('results').first
|
328
|
+
response_body = JSON.parse(response_body) if response_body.is_a?(String)
|
353
329
|
|
354
|
-
|
355
|
-
|
330
|
+
return {
|
331
|
+
'results' => [{
|
332
|
+
'code' => 400,
|
333
|
+
'status' => response_body.nil? ? 'Bad Request' : response_body
|
334
|
+
}]
|
335
|
+
}
|
356
336
|
|
357
|
-
rescue RestClient::
|
337
|
+
rescue RestClient::Unauthorized
|
358
338
|
|
359
|
-
|
339
|
+
return {
|
340
|
+
'code' => 401,
|
341
|
+
'status' => format('Not authorized to connect \'%s\' - wrong username or password?', @icinga_api_url_base)
|
342
|
+
}
|
360
343
|
|
361
|
-
|
344
|
+
rescue RestClient::NotFound
|
362
345
|
|
363
|
-
|
346
|
+
return {
|
347
|
+
'results' => [{
|
348
|
+
'code' => 404,
|
349
|
+
'status' => 'Object not Found'
|
350
|
+
}]
|
351
|
+
}
|
364
352
|
|
365
|
-
|
366
|
-
return {
|
367
|
-
status: error.dig( 'error' ).to_i,
|
368
|
-
message: error.dig( 'status' )
|
369
|
-
}
|
370
|
-
else
|
353
|
+
rescue RestClient::InternalServerError
|
371
354
|
|
372
|
-
|
355
|
+
response_body = JSON.parse(@response_body) if @response_body.is_a?(String)
|
356
|
+
|
357
|
+
results = response_body.dig('results')
|
358
|
+
results = results.first if( results.is_a?(Array) )
|
359
|
+
status = results.dig('status')
|
360
|
+
errors = results.dig('errors')
|
361
|
+
errors = errors.first if( errors.is_a?(Array) )
|
362
|
+
errors = errors.sub(/ \'.*\'/,'')
|
363
|
+
|
364
|
+
return {
|
365
|
+
'results' => [{
|
366
|
+
'code' => 500,
|
367
|
+
'status' => format('%s (%s)', status, errors).delete('.')
|
368
|
+
}]
|
369
|
+
}
|
373
370
|
|
374
|
-
return {
|
375
|
-
status: results.dig('code').to_i,
|
376
|
-
name: results.dig('name'),
|
377
|
-
message: results.dig('status'),
|
378
|
-
error: results.dig('errors')
|
379
|
-
}
|
380
|
-
end
|
381
371
|
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
382
372
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
sleep(2)
|
387
|
-
retry
|
388
|
-
else
|
373
|
+
# TODO
|
374
|
+
# ist hier ein raise sinnvoll?
|
375
|
+
raise format( "Maximum retries (%d) against '%s' reached. Giving up ...", max_retries, @icinga_api_url_base ) if( retried >= max_retries )
|
389
376
|
|
390
|
-
|
391
|
-
|
377
|
+
retried += 1
|
378
|
+
$stderr.puts(format("Cannot execute request against '%s': '%s' (retry %d / %d)", @icinga_api_url_base, e, retried, max_retries))
|
379
|
+
sleep(3)
|
380
|
+
retry
|
392
381
|
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
382
|
+
rescue RestClient::ExceptionWithResponse => e
|
383
|
+
|
384
|
+
@logger.error( "Error: #{__method__} #{method_type.upcase} on #{endpoint} error: '#{e}'" )
|
385
|
+
@logger.error( data )
|
386
|
+
@logger.error( @headers )
|
387
|
+
@logger.error( JSON.pretty_generate( response_headers ) )
|
388
|
+
|
389
|
+
|
390
|
+
return {
|
391
|
+
'results' => [{
|
392
|
+
'code' => 500,
|
393
|
+
'status' => e
|
394
|
+
}]
|
395
|
+
}
|
398
396
|
end
|
399
397
|
|
400
|
-
result
|
401
398
|
end
|
402
399
|
|
400
|
+
|
403
401
|
end
|
404
402
|
end
|