ovirt-engine-sdk 4.0.1 → 4.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGES.adoc +684 -0
- data/README.adoc +729 -32
- data/ext/ovirtsdk4c/extconf.rb +31 -5
- data/ext/ovirtsdk4c/ov_error.c +9 -2
- data/ext/ovirtsdk4c/ov_error.h +3 -1
- data/ext/ovirtsdk4c/ov_http_client.c +1218 -0
- data/ext/ovirtsdk4c/ov_http_client.h +75 -0
- data/ext/ovirtsdk4c/ov_http_request.c +397 -0
- data/ext/ovirtsdk4c/ov_http_request.h +54 -0
- data/ext/ovirtsdk4c/ov_http_response.c +210 -0
- data/ext/ovirtsdk4c/ov_http_response.h +41 -0
- data/ext/ovirtsdk4c/ov_http_transfer.c +91 -0
- data/ext/ovirtsdk4c/ov_http_transfer.h +47 -0
- data/ext/ovirtsdk4c/ov_module.h +2 -2
- data/ext/ovirtsdk4c/ov_string.c +43 -0
- data/ext/ovirtsdk4c/ov_string.h +25 -0
- data/ext/ovirtsdk4c/ov_xml_reader.c +115 -99
- data/ext/ovirtsdk4c/ov_xml_reader.h +20 -3
- data/ext/ovirtsdk4c/ov_xml_writer.c +95 -77
- data/ext/ovirtsdk4c/ov_xml_writer.h +18 -3
- data/ext/ovirtsdk4c/ovirtsdk4c.c +10 -2
- data/lib/ovirtsdk4/connection.rb +695 -0
- data/lib/ovirtsdk4/errors.rb +70 -0
- data/lib/ovirtsdk4/probe.rb +324 -0
- data/lib/ovirtsdk4/reader.rb +74 -40
- data/lib/ovirtsdk4/readers.rb +3325 -976
- data/lib/ovirtsdk4/service.rb +439 -48
- data/lib/ovirtsdk4/services.rb +29365 -21180
- data/lib/ovirtsdk4/type.rb +20 -6
- data/lib/ovirtsdk4/types.rb +15048 -3198
- data/lib/ovirtsdk4/version.rb +1 -1
- data/lib/ovirtsdk4/writer.rb +108 -13
- data/lib/ovirtsdk4/writers.rb +1373 -294
- data/lib/ovirtsdk4.rb +4 -2
- metadata +88 -36
- data/lib/ovirtsdk4/http.rb +0 -548
data/lib/ovirtsdk4/service.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright (c) 2015-
|
2
|
+
# Copyright (c) 2015-2017 Red Hat, Inc.
|
3
3
|
#
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
5
|
# you may not use this file except in compliance with the License.
|
@@ -15,43 +15,77 @@
|
|
15
15
|
#
|
16
16
|
|
17
17
|
module OvirtSDK4
|
18
|
+
#
|
19
|
+
# Instances of this class are returned for operatoins that specify the `wait: false` parameter.
|
20
|
+
#
|
21
|
+
class Future
|
22
|
+
#
|
23
|
+
# Creates a new future result.
|
24
|
+
#
|
25
|
+
# @param service [Service] The service that created this future.
|
26
|
+
# @param request [HttpRequest] The request that this future will wait for when the `wait` method is called.
|
27
|
+
# @param block [Block] The block that will be executed to check the response, and to convert its body into the
|
28
|
+
# right type of object.
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
#
|
32
|
+
def initialize(service, request, &block)
|
33
|
+
@service = service
|
34
|
+
@request = request
|
35
|
+
@block = block
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Waits till the result of the operation that created this future is available.
|
40
|
+
#
|
41
|
+
# @return [Object] The result of the operation that created this future.
|
42
|
+
#
|
43
|
+
def wait
|
44
|
+
response = @service.connection.wait(@request)
|
45
|
+
raise response if response.is_a?(Exception)
|
46
|
+
|
47
|
+
@block.call(response)
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Returns a string representation of the future.
|
52
|
+
#
|
53
|
+
# @return [String] The string representation.
|
54
|
+
#
|
55
|
+
def inspect
|
56
|
+
"#<#{self.class.name}:#{@request.method} #{@request.url}>"
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Returns a string representation of the future.
|
61
|
+
#
|
62
|
+
# @return [String] The string representation.
|
63
|
+
#
|
64
|
+
def to_s
|
65
|
+
inspect
|
66
|
+
end
|
67
|
+
end
|
18
68
|
|
19
69
|
#
|
20
70
|
# This is the base class for all the services of the SDK. It contains the utility methods used by all of them.
|
21
71
|
#
|
22
72
|
class Service
|
23
|
-
|
24
73
|
#
|
25
|
-
# Creates
|
74
|
+
# Creates a new implementation of the service.
|
26
75
|
#
|
27
|
-
#
|
28
|
-
#
|
76
|
+
# @param parent [Service, Connection] The parent of this service. For most services the parent will be another
|
77
|
+
# service. For example, for the `vm` service that manages virtual machine `123` the parent will be the `vms`
|
78
|
+
# service that manages the collection of virtual machines. For the root of the services tree the parent will
|
79
|
+
# be the connection.
|
80
|
+
#
|
81
|
+
# @param path [String] The path of this service, relative to its parent. For example, the path of the `vm`
|
82
|
+
# service that manages virtual machine `123` will be `vm/123`.
|
29
83
|
#
|
30
84
|
# @api private
|
31
85
|
#
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
unless fault.reason.nil?
|
36
|
-
message << ' ' unless message.empty?
|
37
|
-
message << "Fault reason is \"#{fault.reason}\"."
|
38
|
-
end
|
39
|
-
unless fault.detail.nil?
|
40
|
-
message << ' ' unless message.empty?
|
41
|
-
message << "Fault detail is \"#{fault.detail}\"."
|
42
|
-
end
|
43
|
-
end
|
44
|
-
unless response.nil?
|
45
|
-
unless response.code.nil?
|
46
|
-
message << ' ' unless message.empty?
|
47
|
-
message << "HTTP response code is #{response.code}."
|
48
|
-
end
|
49
|
-
unless response.message.nil?
|
50
|
-
message << ' ' unless message.empty?
|
51
|
-
message << "HTTP response message is \"#{response.message}\"."
|
52
|
-
end
|
53
|
-
end
|
54
|
-
raise Error.new(message)
|
86
|
+
def initialize(parent, path)
|
87
|
+
@parent = parent
|
88
|
+
@path = path
|
55
89
|
end
|
56
90
|
|
57
91
|
#
|
@@ -63,15 +97,9 @@ module OvirtSDK4
|
|
63
97
|
# @api private
|
64
98
|
#
|
65
99
|
def check_fault(response)
|
66
|
-
body = response
|
67
|
-
|
68
|
-
|
69
|
-
end
|
70
|
-
body = Reader.read(body)
|
71
|
-
if body.is_a?(Fault)
|
72
|
-
raise_error(response, body)
|
73
|
-
end
|
74
|
-
raise Error.new("Expected a fault, but got '#{body.class.name.split('::').last}'")
|
100
|
+
body = internal_read_body(response)
|
101
|
+
connection.raise_error(response, body) if body.is_a?(Fault)
|
102
|
+
raise Error, "Expected a fault, but got '#{body.class.name.split('::').last}'"
|
75
103
|
end
|
76
104
|
|
77
105
|
#
|
@@ -85,21 +113,384 @@ module OvirtSDK4
|
|
85
113
|
# @api private
|
86
114
|
#
|
87
115
|
def check_action(response)
|
88
|
-
body = response
|
89
|
-
|
90
|
-
raise_error(response, nil)
|
91
|
-
end
|
92
|
-
body = Reader.read(body)
|
93
|
-
if body.is_a?(Fault)
|
94
|
-
raise_error(response, body)
|
95
|
-
end
|
116
|
+
body = internal_read_body(response)
|
117
|
+
connection.raise_error(response, body) if body.is_a?(Fault)
|
96
118
|
if body.is_a?(Action)
|
97
119
|
return body if body.fault.nil?
|
98
|
-
|
120
|
+
|
121
|
+
connection.raise_error(response, body.fault)
|
99
122
|
end
|
100
|
-
raise Error
|
123
|
+
raise Error, "Expected an action or a fault, but got '#{body.class.name.split('::').last}'"
|
101
124
|
end
|
102
125
|
|
103
|
-
|
126
|
+
#
|
127
|
+
# Returns the connection used by this service.
|
128
|
+
#
|
129
|
+
# This method is intended for internal use by other components of the SDK. Refrain from using it directly, as
|
130
|
+
# backwards compatibility isn't guaranteed.
|
131
|
+
#
|
132
|
+
# @return [Connection] The connection used by this service.
|
133
|
+
#
|
134
|
+
# @api private
|
135
|
+
#
|
136
|
+
def connection
|
137
|
+
return @parent if @parent.is_a? Connection
|
138
|
+
|
139
|
+
@parent.connection
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Returns a string representation of the service.
|
144
|
+
#
|
145
|
+
# @return [String] The string representation.
|
146
|
+
#
|
147
|
+
def inspect
|
148
|
+
"#<#{self.class.name}:#{absolute_path}>"
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# Returns a string representation of the service.
|
153
|
+
#
|
154
|
+
# @return [String] The string representation.
|
155
|
+
#
|
156
|
+
def to_s
|
157
|
+
inspect
|
158
|
+
end
|
159
|
+
|
160
|
+
protected
|
161
|
+
|
162
|
+
#
|
163
|
+
# Executes a `get` method.
|
164
|
+
#
|
165
|
+
# @param specs [Array<Array<Symbol, Class>>] An array of arrays containing the names and types of the parameters.
|
166
|
+
# @param opts [Hash] The hash containing the values of the parameters.
|
167
|
+
#
|
168
|
+
# @api private
|
169
|
+
#
|
170
|
+
def internal_get(specs, opts)
|
171
|
+
# Get the values of the built-in options:
|
172
|
+
headers = opts.delete(:headers) || {}
|
173
|
+
query = opts.delete(:query) || {}
|
174
|
+
timeout = opts.delete(:timeout)
|
175
|
+
wait = opts.delete(:wait)
|
176
|
+
wait = true if wait.nil?
|
177
|
+
|
178
|
+
# Get the values of the options specific to this operation:
|
179
|
+
specs.each do |name, kind|
|
180
|
+
value = opts.delete(name)
|
181
|
+
query[name] = Writer.render(value, kind) unless value.nil?
|
182
|
+
end
|
183
|
+
|
184
|
+
# Check the remaining options:
|
185
|
+
check_bad_opts(specs, opts)
|
186
|
+
|
187
|
+
# Create and send the request:
|
188
|
+
request = HttpRequest.new
|
189
|
+
request.method = :GET
|
190
|
+
request.url = absolute_path
|
191
|
+
request.headers = headers
|
192
|
+
request.query = query
|
193
|
+
request.timeout = timeout
|
194
|
+
connection.send(request)
|
195
|
+
result = Future.new(self, request) do |response|
|
196
|
+
raise response if response.is_a?(Exception)
|
197
|
+
|
198
|
+
case response.code
|
199
|
+
when 200
|
200
|
+
internal_read_body(response)
|
201
|
+
else
|
202
|
+
check_fault(response)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
result = result.wait if wait
|
206
|
+
result
|
207
|
+
end
|
208
|
+
|
209
|
+
#
|
210
|
+
# Executes an `add` method.
|
211
|
+
#
|
212
|
+
# @param object [Object] The added object.
|
213
|
+
# @param type [Class] Type type of the added object.
|
214
|
+
# @param specs [Array<Array<Symbol, Class>>] An array of arrays containing the names and types of the parameters.
|
215
|
+
# @param opts [Hash] The hash containing the values of the parameters.
|
216
|
+
#
|
217
|
+
# @api private
|
218
|
+
#
|
219
|
+
def internal_add(object, type, specs, opts)
|
220
|
+
# Get the values of the built-in options:
|
221
|
+
object = type.new(object) if object.is_a?(Hash)
|
222
|
+
headers = opts.delete(:headers) || {}
|
223
|
+
query = opts.delete(:query) || {}
|
224
|
+
timeout = opts.delete(:timeout)
|
225
|
+
wait = opts.delete(:wait)
|
226
|
+
wait = true if wait.nil?
|
227
|
+
|
228
|
+
# Get the values of the options specific to this operation:
|
229
|
+
specs.each do |name, kind|
|
230
|
+
value = opts.delete(name)
|
231
|
+
query[name] = Writer.render(value, kind) unless value.nil?
|
232
|
+
end
|
233
|
+
|
234
|
+
# Check the remaining options:
|
235
|
+
check_bad_opts(specs, opts)
|
236
|
+
|
237
|
+
# Create and send the request:
|
238
|
+
request = HttpRequest.new
|
239
|
+
request.method = :POST
|
240
|
+
request.url = absolute_path
|
241
|
+
request.headers = headers
|
242
|
+
request.query = query
|
243
|
+
request.body = Writer.write(object, indent: true)
|
244
|
+
request.timeout = timeout
|
245
|
+
connection.send(request)
|
246
|
+
result = Future.new(self, request) do |response|
|
247
|
+
raise response if response.is_a?(Exception)
|
248
|
+
|
249
|
+
case response.code
|
250
|
+
when 200, 201, 202
|
251
|
+
internal_read_body(response)
|
252
|
+
else
|
253
|
+
check_fault(response)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
result = result.wait if wait
|
257
|
+
result
|
258
|
+
end
|
259
|
+
|
260
|
+
#
|
261
|
+
# Executes an `update` method.
|
262
|
+
#
|
263
|
+
# @param object [Object] The updated object.
|
264
|
+
# @param type [Class] Type type of the updated object.
|
265
|
+
# @param specs [Array<Array<Symbol, Class>>] An array of tuples containing the names and types of the parameters.
|
266
|
+
# @param opts [Hash] The hash containing the values of the parameters.
|
267
|
+
#
|
268
|
+
# @api private
|
269
|
+
#
|
270
|
+
def internal_update(object, type, specs, opts)
|
271
|
+
# get the values of the built-in options:
|
272
|
+
object = type.new(object) if object.is_a?(Hash)
|
273
|
+
headers = opts.delete(:headers) || {}
|
274
|
+
query = opts.delete(:query) || {}
|
275
|
+
timeout = opts.delete(:timeout)
|
276
|
+
wait = opts.delete(:wait)
|
277
|
+
wait = true if wait.nil?
|
278
|
+
|
279
|
+
# Get the values of the options specific to this operation:
|
280
|
+
specs.each do |name, kind|
|
281
|
+
value = opts.delete(name)
|
282
|
+
query[name] = Writer.render(value, kind) unless value.nil?
|
283
|
+
end
|
284
|
+
|
285
|
+
# Check the remaining options:
|
286
|
+
check_bad_opts(specs, opts)
|
287
|
+
|
288
|
+
# Create and send the request:
|
289
|
+
request = HttpRequest.new
|
290
|
+
request.method = :PUT
|
291
|
+
request.url = absolute_path
|
292
|
+
request.headers = headers
|
293
|
+
request.query = query
|
294
|
+
request.body = Writer.write(object, indent: true)
|
295
|
+
request.timeout = timeout
|
296
|
+
connection.send(request)
|
297
|
+
result = Future.new(self, request) do |response|
|
298
|
+
raise response if response.is_a?(Exception)
|
299
|
+
|
300
|
+
case response.code
|
301
|
+
when 200
|
302
|
+
internal_read_body(response)
|
303
|
+
else
|
304
|
+
check_fault(response)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
result = result.wait if wait
|
308
|
+
result
|
309
|
+
end
|
104
310
|
|
311
|
+
#
|
312
|
+
# Executes a `remove` method.
|
313
|
+
#
|
314
|
+
# @param specs [Array<Array<Symbol, Class>>] An array of tuples containing the names and types of the parameters.
|
315
|
+
# @param opts [Hash] The hash containing the values of the parameters.
|
316
|
+
#
|
317
|
+
# @api private
|
318
|
+
#
|
319
|
+
def internal_remove(specs, opts)
|
320
|
+
# Get the values of the built-in options:
|
321
|
+
headers = opts.delete(:headers) || {}
|
322
|
+
query = opts.delete(:query) || {}
|
323
|
+
timeout = opts.delete(:timeout)
|
324
|
+
wait = opts.delete(:wait)
|
325
|
+
wait = true if wait.nil?
|
326
|
+
|
327
|
+
# Get the values of the options specific to this operation:
|
328
|
+
specs.each do |name, kind|
|
329
|
+
value = opts.delete(name)
|
330
|
+
query[name] = Writer.render(value, kind) unless value.nil?
|
331
|
+
end
|
332
|
+
|
333
|
+
# Check the remaining options:
|
334
|
+
check_bad_opts(specs, opts)
|
335
|
+
|
336
|
+
# Create and send the request:
|
337
|
+
request = HttpRequest.new
|
338
|
+
request.method = :DELETE
|
339
|
+
request.url = absolute_path
|
340
|
+
request.headers = headers
|
341
|
+
request.query = query
|
342
|
+
request.timeout = timeout
|
343
|
+
connection.send(request)
|
344
|
+
result = Future.new(self, request) do |response|
|
345
|
+
raise response if response.is_a?(Exception)
|
346
|
+
|
347
|
+
check_fault(response) unless response.code == 200
|
348
|
+
end
|
349
|
+
result = result.wait if wait
|
350
|
+
result
|
351
|
+
end
|
352
|
+
|
353
|
+
#
|
354
|
+
# Executes an action method.
|
355
|
+
#
|
356
|
+
# @param name [Symbol] The name of the action, for example `:start`.
|
357
|
+
# @param member [Symbol] The name of the action member that contains the result. For example `:is_attached`. Can
|
358
|
+
# be `nil` if the action doesn't return any value.
|
359
|
+
# @param specs [Array<Array<Symbol, Class>>] An array of tuples containing the names and types of the parameters.
|
360
|
+
# @param opts [Hash] The hash containing the parameters of the action.
|
361
|
+
#
|
362
|
+
# @api private
|
363
|
+
#
|
364
|
+
def internal_action(name, member, specs, opts)
|
365
|
+
# Get the values of the built-in options:
|
366
|
+
headers = opts.delete(:headers) || {}
|
367
|
+
query = opts.delete(:query) || {}
|
368
|
+
timeout = opts.delete(:timeout)
|
369
|
+
wait = opts.delete(:wait)
|
370
|
+
wait = true if wait.nil?
|
371
|
+
|
372
|
+
# Create the action:
|
373
|
+
action = Action.new(opts)
|
374
|
+
|
375
|
+
# The constructor of the action doesn't remove the options that it uses, so we need to remove them explicitly
|
376
|
+
# before checking for bad options.
|
377
|
+
specs.each_entry do |key, _|
|
378
|
+
opts.delete(key)
|
379
|
+
end
|
380
|
+
check_bad_opts(specs, opts)
|
381
|
+
|
382
|
+
# Create and send the request:
|
383
|
+
request = HttpRequest.new
|
384
|
+
request.method = :POST
|
385
|
+
request.url = "#{absolute_path}/#{name}"
|
386
|
+
request.headers = headers
|
387
|
+
request.query = query
|
388
|
+
request.body = Writer.write(action, indent: true)
|
389
|
+
request.timeout = timeout
|
390
|
+
connection.send(request)
|
391
|
+
result = Future.new(self, request) do |response|
|
392
|
+
raise response if response.is_a?(Exception)
|
393
|
+
|
394
|
+
case response.code
|
395
|
+
when 200, 201, 202
|
396
|
+
action = check_action(response)
|
397
|
+
action.send(member) if member
|
398
|
+
else
|
399
|
+
check_action(response)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
result = result.wait if wait
|
403
|
+
result
|
404
|
+
end
|
405
|
+
|
406
|
+
#
|
407
|
+
# Checks the content type of the given response, and if it is XML, as expected, reads the body and converts it
|
408
|
+
# to an object. If it isn't XML, then it raises an exception.
|
409
|
+
#
|
410
|
+
# @param response [HttpResponse] The HTTP response to check.
|
411
|
+
# @return [Object] The result of converting the HTTP response body from XML to an SDK object.
|
412
|
+
#
|
413
|
+
# @api private
|
414
|
+
#
|
415
|
+
def internal_read_body(response)
|
416
|
+
# First check if the response body is empty, as it makes no sense to check the content type if there is
|
417
|
+
# no body:
|
418
|
+
connection.raise_error(response, 'The response body is empty') if response.body.nil? || response.body.length.zero?
|
419
|
+
|
420
|
+
# Check the content type, as otherwise the parsing will fail, and the resulting error message won't be explicit
|
421
|
+
# about the cause of the problem:
|
422
|
+
connection.check_xml_content_type(response)
|
423
|
+
|
424
|
+
# Parse the XML and generate the SDK object:
|
425
|
+
Reader.read(response.body)
|
426
|
+
end
|
427
|
+
|
428
|
+
#
|
429
|
+
# Returns the absolute path of this service.
|
430
|
+
#
|
431
|
+
# @return [String] The absolute path of this service. For example, the path of the `vm` service that manages
|
432
|
+
# virtual machine `123` will be `vms/123`. Note that this absolute path doesn't include the `/ovirt-engine/api/'
|
433
|
+
# prefix.
|
434
|
+
#
|
435
|
+
# @api private
|
436
|
+
#
|
437
|
+
def absolute_path
|
438
|
+
return @path if @parent.is_a? Connection
|
439
|
+
|
440
|
+
prefix = @parent.absolute_path
|
441
|
+
return @path if prefix.empty?
|
442
|
+
|
443
|
+
"#{prefix}/#{@path}"
|
444
|
+
end
|
445
|
+
|
446
|
+
private
|
447
|
+
|
448
|
+
#
|
449
|
+
# Checks if the given hash contains any value, and if it does raises an exception indicating that they are not
|
450
|
+
# supported.
|
451
|
+
#
|
452
|
+
# @param specs [Array<Array<Symbol, Class>>] An array of tuples containing the names and types of the parameters.
|
453
|
+
# @param opts [Hash] The hash containing the values of the parameters.
|
454
|
+
#
|
455
|
+
def check_bad_opts(specs, opts)
|
456
|
+
return if opts.empty?
|
457
|
+
|
458
|
+
bad_names = opts.keys
|
459
|
+
bad_text = nice_list(bad_names)
|
460
|
+
if bad_names.length > 1
|
461
|
+
message = "The options #{bad_text} aren't supported."
|
462
|
+
else
|
463
|
+
message = "The option #{bad_text} isn't supported."
|
464
|
+
end
|
465
|
+
good_names = specs.map(&:first)
|
466
|
+
unless good_names.empty?
|
467
|
+
good_text = nice_list(good_names)
|
468
|
+
if good_names.length > 1
|
469
|
+
message << " The supported options are #{good_text}."
|
470
|
+
else
|
471
|
+
message << " The only supported option is #{good_text}."
|
472
|
+
end
|
473
|
+
end
|
474
|
+
raise Error, message
|
475
|
+
end
|
476
|
+
|
477
|
+
#
|
478
|
+
# Generates a human readable list containing the names of the given symbols.
|
479
|
+
#
|
480
|
+
# @param items [Array<Symbol>]
|
481
|
+
# @return [String] An string containing the names of the symbols, sorted, quoted, and in a gramatically correct
|
482
|
+
# format.
|
483
|
+
#
|
484
|
+
def nice_list(items)
|
485
|
+
return nil if items.empty?
|
486
|
+
|
487
|
+
items = items.sort
|
488
|
+
items = items.map { |item| "'#{item}'" }
|
489
|
+
return items.first if items.length == 1
|
490
|
+
|
491
|
+
head = items[0, items.length - 1].join(', ')
|
492
|
+
tail = items.last
|
493
|
+
"#{head} and #{tail}"
|
494
|
+
end
|
495
|
+
end
|
105
496
|
end
|