netsnmp 0.1.8 → 0.4.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 +60 -27
- data/lib/netsnmp.rb +3 -21
- data/lib/netsnmp/client.rb +4 -5
- data/lib/netsnmp/encryption/aes.rb +1 -3
- data/lib/netsnmp/encryption/des.rb +0 -2
- data/lib/netsnmp/errors.rb +1 -0
- data/lib/netsnmp/extensions.rb +113 -0
- data/lib/netsnmp/loggable.rb +36 -0
- data/lib/netsnmp/message.rb +70 -28
- data/lib/netsnmp/mib.rb +172 -0
- data/lib/netsnmp/mib/parser.rb +750 -0
- data/lib/netsnmp/oid.rb +7 -12
- data/lib/netsnmp/pdu.rb +23 -12
- data/lib/netsnmp/scoped_pdu.rb +8 -2
- data/lib/netsnmp/security_parameters.rb +22 -14
- data/lib/netsnmp/session.rb +14 -16
- data/lib/netsnmp/v3_session.rb +21 -9
- data/lib/netsnmp/varbind.rb +27 -22
- data/lib/netsnmp/version.rb +1 -1
- data/sig/client.rbs +24 -0
- data/sig/loggable.rbs +16 -0
- data/sig/message.rbs +9 -0
- data/sig/mib.rbs +21 -0
- data/sig/mib/parser.rbs +7 -0
- data/sig/netsnmp.rbs +19 -0
- data/sig/oid.rbs +18 -0
- data/sig/openssl.rbs +20 -0
- data/sig/pdu.rbs +48 -0
- data/sig/scoped_pdu.rbs +15 -0
- data/sig/security_parameters.rbs +58 -0
- data/sig/session.rbs +38 -0
- data/sig/timeticks.rbs +7 -0
- data/sig/v3_session.rbs +21 -0
- data/sig/varbind.rbs +30 -0
- data/spec/client_spec.rb +26 -8
- data/spec/handlers/celluloid_spec.rb +4 -3
- data/spec/mib_spec.rb +13 -0
- data/spec/session_spec.rb +2 -2
- data/spec/spec_helper.rb +9 -5
- data/spec/support/request_examples.rb +2 -2
- data/spec/v3_session_spec.rb +4 -4
- data/spec/varbind_spec.rb +5 -3
- metadata +31 -71
- data/.coveralls.yml +0 -1
- data/.gitignore +0 -14
- data/.rspec +0 -2
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -69
- data/.travis.yml +0 -28
- data/Gemfile +0 -22
- data/Rakefile +0 -30
- data/netsnmp.gemspec +0 -31
- data/spec/support/Dockerfile +0 -14
- data/spec/support/specs.sh +0 -51
- data/spec/support/stop_docker.sh +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c87bf65264474758aed741b992dadbd6bbb277aae52b925254da2e5fea56c5b3
|
4
|
+
data.tar.gz: cf7134f2e8b451c619aabdf9dcd6b931404ab51afd7953c785e4013a348cce02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1cade51302613f51f8da018351401b27c2d0fe08a43b915a79252ff420d5efdfe4a405ce29a60f10ade2d0f0f4a03dc2302117653043c451dd01683e8eb2c7a5
|
7
|
+
data.tar.gz: 0ce54aa2fb2cba42f47ccab2fd83e2536e8fd9e73d29d2f7a85aef4ab90d9e0c956f64f0f8fbe19f95c36a048ab2911492f55c4ca95ada0843941a896564b2cf
|
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# netsnmp
|
2
2
|
|
3
|
-
|
4
|
-
[](https://coveralls.io/github/swisscom/ruby-netsnmp?branch=master)
|
3
|
+

|
5
4
|
[](https://codeclimate.com/github/swisscom/ruby-netsnmp)
|
6
5
|
[](https://www.rubydoc.info/github/swisscom/ruby-netsnmp/master)
|
7
6
|
|
@@ -33,7 +32,7 @@ This gem provides:
|
|
33
32
|
|
34
33
|
* Implementation in ruby of the SNMP Protocol for v3, v2c and v1 (most notable the rfc3414 and 3826).
|
35
34
|
* Client/Manager API with simple interface for get, genext, set and walk.
|
36
|
-
*
|
35
|
+
* Pure Ruby.
|
37
36
|
* Support for concurrency and evented I/O.
|
38
37
|
|
39
38
|
## Why?
|
@@ -55,10 +54,12 @@ All of these issues are resolved here.
|
|
55
54
|
## Features
|
56
55
|
|
57
56
|
* Client Interface, which supports SNMP v3, v2c, and v1
|
58
|
-
* Supports get, getnext, set and walk calls
|
57
|
+
* Supports get, getnext, set and walk calls
|
58
|
+
* MIB support
|
59
59
|
* Proxy IO object support (for eventmachine/celluloid-io)
|
60
60
|
* Ruby >= 2.1 support (modern)
|
61
61
|
* Pure Ruby (no FFI)
|
62
|
+
* Easy PDU debugging
|
62
63
|
|
63
64
|
## Examples
|
64
65
|
|
@@ -74,12 +75,11 @@ manager = NETSNMP::Client.new(host: "localhost", port: 33445, username: "simulat
|
|
74
75
|
context: "a172334d7d97871b72241397f713fa12")
|
75
76
|
|
76
77
|
# SNMP get
|
77
|
-
|
78
|
-
manager.get(oid: "1.3.6.1.2.1.1.0") #=> 'tt'
|
78
|
+
manager.get(oid: "sysName.0") #=> 'tt'
|
79
79
|
|
80
80
|
# SNMP walk
|
81
81
|
# sysORDescr
|
82
|
-
manager.walk(oid: "
|
82
|
+
manager.walk(oid: "sysORDescr").each do |oid_code, value|
|
83
83
|
# do something with them
|
84
84
|
puts "for #{oid_code}: #{value}"
|
85
85
|
end
|
@@ -136,6 +136,29 @@ manager.set("somecounteroid", value: 999999, type: 6)
|
|
136
136
|
```
|
137
137
|
* Fork this library, extend support, write a test and submit a PR (the desired solution ;) )
|
138
138
|
|
139
|
+
## MIB
|
140
|
+
|
141
|
+
`netsnmp` will load the default MIBs from known or advertised (via `MIBDIRS`) directories (provided that they're installed in the system). These will be used for the OID conversion.
|
142
|
+
|
143
|
+
Sometimes you'll need to load more, your own MIBs, in which case, you can use the following API:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
require "netsnmp"
|
147
|
+
|
148
|
+
NETSNMP::MIB.load("MY-MIB")
|
149
|
+
# or, if it's not in any of the known locations
|
150
|
+
NETSNMP::MIB.load("/path/to/MY-MIB.txt")
|
151
|
+
```
|
152
|
+
|
153
|
+
You can install common SNMP mibs by using your package manager:
|
154
|
+
|
155
|
+
```
|
156
|
+
# using apt-get
|
157
|
+
> apt-get install snmp-mibs-downloader
|
158
|
+
# using apk
|
159
|
+
> apk --update add net-snmp-libs
|
160
|
+
```
|
161
|
+
|
139
162
|
## Concurrency
|
140
163
|
|
141
164
|
In ruby, you are usually adviced not to share IO objects across threads. The same principle applies here to `NETSNMP::Client`: provided you use it within a thread of execution, it should behave safely. So, something like this would be possible:
|
@@ -214,15 +237,32 @@ NETSNMP::Client.new(share_options.merge(proxy: router_proxy, security_parameters
|
|
214
237
|
end
|
215
238
|
```
|
216
239
|
|
240
|
+
## Compatibility
|
241
|
+
|
242
|
+
This library supports and is tested against ruby versions 2.1 or more recent, including ruby 3. It also supports and tests against Truffleruby.
|
243
|
+
|
217
244
|
## OpenSSL
|
218
245
|
|
219
246
|
All encoding/decoding/encryption/decryption/digests are done using `openssl`, which is (still) a part of the standard library. If at some point `openssl` is removed and not specifically distributed, you'll have to install it yourself. Hopefully this will never happen.
|
220
247
|
|
248
|
+
It also uses the `openssl` ASN.1 API to encode/decode BERs, which is known to be strict, and [may not be able to decode PDUs if not compliant with the supported RFC](https://github.com/swisscom/ruby-netsnmp/issues/47).
|
249
|
+
|
250
|
+
## Debugging
|
251
|
+
|
252
|
+
You can either set the `NETSNMP_DEBUG` to the desided debug level (currently, 1 and 2). The logs will be written to stderr.
|
253
|
+
|
254
|
+
You can also set it for a specific client:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
manager2 = NETSNMP::Client.new(debug: $stderr, debug_level: 2, ....)
|
258
|
+
```
|
259
|
+
|
221
260
|
|
222
261
|
## Tests
|
223
262
|
|
224
263
|
This library uses RSpec. The client specs are "integration" tests, in that we communicate with an [snmpsim-built snmp agent simulator](https://github.com/etingof/snmpsim).
|
225
264
|
|
265
|
+
|
226
266
|
### RSpec
|
227
267
|
|
228
268
|
You can run all tests by typing:
|
@@ -234,37 +274,31 @@ You can run all tests by typing:
|
|
234
274
|
...
|
235
275
|
```
|
236
276
|
|
237
|
-
### SNMP Simulator
|
238
|
-
|
239
|
-
You can install the package yourself (ex: `pip install snmpsim`) and run the server locally, and then set the `SNMP_PORT` environment variable, where the snmp simulator is running.
|
240
|
-
|
241
|
-
#### Docker
|
242
277
|
|
243
|
-
|
278
|
+
### Docker
|
244
279
|
|
245
|
-
|
280
|
+
The most straightforward way of running the tests is by using the `docker-compose` setup (which is also what's used in the CI). Run it against the ruby version you're targeting:
|
246
281
|
|
247
282
|
```
|
248
|
-
>
|
283
|
+
> docker-compose -f docker-compose.yml -f docker-compose-ruby-${RUBY_MAJOR_VERSION}.${RUBY_MAJOR_VERSION}.yml run netsnmp
|
249
284
|
```
|
250
285
|
|
251
|
-
|
286
|
+
The CI runs the tests against all supported ruby versions. If changes break a specific version of ruby, make sure you commit appropriate changes addressing the edge case, or let me know in the issues board, so I can help.
|
252
287
|
|
253
|
-
|
254
|
-
> docker port test-snmp 1161/udp
|
255
|
-
```
|
288
|
+
### SNMP Simulator
|
256
289
|
|
257
|
-
|
290
|
+
The SNMP simulator runs in its own container in the `docker` setup.
|
258
291
|
|
259
|
-
|
292
|
+
You can install the package yourself (ex: `pip install snmpsim`) and run the server locally, and then set the `SNMP_PORT` environment variable, where the snmp simulator is running.
|
260
293
|
|
261
|
-
|
294
|
+
#### CI
|
262
295
|
|
263
|
-
|
264
|
-
> spec/support/spec.sh run
|
265
|
-
```
|
296
|
+
The job of the CI is:
|
266
297
|
|
267
|
-
|
298
|
+
* Run all the tests;
|
299
|
+
* Make sure the tests cover an appropriate surface of the code;
|
300
|
+
* Lint the code;
|
301
|
+
* (for ruby 3.0) type check the code;
|
268
302
|
|
269
303
|
|
270
304
|
## Contributing
|
@@ -278,7 +312,6 @@ Which should: build and run the simulator container, run the tests, run rubocop,
|
|
278
312
|
|
279
313
|
There are some features which this gem doesn't support. It was built to provide a client (or manager, in SNMP language) implementation only, and the requirements were fulfilled. However, these notable misses will stand-out:
|
280
314
|
|
281
|
-
* No MIB support (you can only work with OIDs)
|
282
315
|
* No server (Agent, in SNMP-ish) implementation.
|
283
316
|
* No getbulk support.
|
284
317
|
|
data/lib/netsnmp.rb
CHANGED
@@ -33,34 +33,16 @@ rescue LoadError
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
module NETSNMP
|
37
|
-
def self.debug=(io)
|
38
|
-
@debug_output = io
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.debug(&blk)
|
42
|
-
@debug_output << blk.call + "\n" if @debug_output
|
43
|
-
end
|
44
|
-
|
45
|
-
unless defined?(Hexdump) # support the hexdump gem
|
46
|
-
module Hexdump
|
47
|
-
def self.dump(data, width: 8)
|
48
|
-
pairs = data.unpack("H*").first.scan(/.{4}/)
|
49
|
-
pairs.each_slice(width).map do |row|
|
50
|
-
row.join(" ")
|
51
|
-
end.join("\n")
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
36
|
require "netsnmp/errors"
|
37
|
+
require "netsnmp/extensions"
|
38
|
+
require "netsnmp/loggable"
|
58
39
|
|
59
40
|
require "netsnmp/timeticks"
|
60
41
|
|
61
42
|
require "netsnmp/oid"
|
62
43
|
require "netsnmp/varbind"
|
63
44
|
require "netsnmp/pdu"
|
45
|
+
require "netsnmp/mib"
|
64
46
|
require "netsnmp/session"
|
65
47
|
|
66
48
|
require "netsnmp/scoped_pdu"
|
data/lib/netsnmp/client.rb
CHANGED
@@ -23,8 +23,7 @@ module NETSNMP
|
|
23
23
|
# puts client.get(oid: "1.3.6.1.2.1.1.5.0")
|
24
24
|
# end
|
25
25
|
#
|
26
|
-
def initialize(**options)
|
27
|
-
version = options[:version]
|
26
|
+
def initialize(version: nil, **options)
|
28
27
|
version = case version
|
29
28
|
when Integer then version # assume the use know what he's doing
|
30
29
|
when /v?1/ then 0
|
@@ -33,7 +32,7 @@ module NETSNMP
|
|
33
32
|
end
|
34
33
|
|
35
34
|
@retries = options.fetch(:retries, RETRIES)
|
36
|
-
@session ||= version == 3 ? V3Session.new(options) : Session.new(options)
|
35
|
+
@session ||= version == 3 ? V3Session.new(**options) : Session.new(version: version, **options)
|
37
36
|
return unless block_given?
|
38
37
|
begin
|
39
38
|
yield self
|
@@ -78,7 +77,7 @@ module NETSNMP
|
|
78
77
|
# @return [Enumerator] the enumerator-collection of the oid-value pairs
|
79
78
|
#
|
80
79
|
def walk(oid:)
|
81
|
-
walkoid = oid
|
80
|
+
walkoid = OID.build(oid)
|
82
81
|
Enumerator.new do |y|
|
83
82
|
code = walkoid
|
84
83
|
first_response_code = nil
|
@@ -154,7 +153,7 @@ module NETSNMP
|
|
154
153
|
retries = @retries
|
155
154
|
begin
|
156
155
|
yield
|
157
|
-
rescue Timeout::Error => e
|
156
|
+
rescue Timeout::Error, IdNotInTimeWindowError => e
|
158
157
|
raise e if retries.zero?
|
159
158
|
retries -= 1
|
160
159
|
retry
|
@@ -22,13 +22,12 @@ module NETSNMP
|
|
22
22
|
end
|
23
23
|
|
24
24
|
encrypted_data = cipher.update(decrypted_data) + cipher.final
|
25
|
-
NETSNMP.debug { "encrypted:\n#{Hexdump.dump(encrypted_data)}" }
|
26
25
|
|
27
26
|
[encrypted_data, salt]
|
28
27
|
end
|
29
28
|
|
30
29
|
def decrypt(encrypted_data, salt:, engine_boots:, engine_time:)
|
31
|
-
raise Error, "invalid priv salt received" unless (salt.length % 8).zero?
|
30
|
+
raise Error, "invalid priv salt received" unless !salt.empty? && (salt.length % 8).zero?
|
32
31
|
|
33
32
|
cipher = OpenSSL::Cipher::AES128.new(:CFB)
|
34
33
|
cipher.padding = 0
|
@@ -39,7 +38,6 @@ module NETSNMP
|
|
39
38
|
cipher.key = aes_key
|
40
39
|
cipher.iv = iv
|
41
40
|
decrypted_data = cipher.update(encrypted_data) + cipher.final
|
42
|
-
NETSNMP.debug { "decrypted:\n#{Hexdump.dump(decrypted_data)}" }
|
43
41
|
|
44
42
|
hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
|
45
43
|
decrypted_data.byteslice(0, hlen + bodylen)
|
@@ -24,7 +24,6 @@ module NETSNMP
|
|
24
24
|
end
|
25
25
|
|
26
26
|
encrypted_data = cipher.update(decrypted_data) + cipher.final
|
27
|
-
NETSNMP.debug { "encrypted:\n#{Hexdump.dump(encrypted_data)}" }
|
28
27
|
[encrypted_data, salt]
|
29
28
|
end
|
30
29
|
|
@@ -41,7 +40,6 @@ module NETSNMP
|
|
41
40
|
cipher.key = des_key
|
42
41
|
cipher.iv = iv
|
43
42
|
decrypted_data = cipher.update(encrypted_data) + cipher.final
|
44
|
-
NETSNMP.debug { "decrypted:\n#{Hexdump.dump(decrypted_data)}" }
|
45
43
|
|
46
44
|
hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
|
47
45
|
decrypted_data.byteslice(0, hlen + bodylen)
|
data/lib/netsnmp/errors.rb
CHANGED
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NETSNMP
|
4
|
+
module IsNumericExtensions
|
5
|
+
refine String do
|
6
|
+
def integer?
|
7
|
+
each_byte do |byte|
|
8
|
+
return false unless byte >= 48 && byte <= 57
|
9
|
+
end
|
10
|
+
true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module StringExtensions
|
16
|
+
refine(String) do
|
17
|
+
unless String.method_defined?(:match?)
|
18
|
+
def match?(*args)
|
19
|
+
!match(*args).nil?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
unless String.method_defined?(:unpack1)
|
24
|
+
def unpack1(format)
|
25
|
+
unpack(format).first
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ASNExtensions
|
32
|
+
ASN_COLORS = {
|
33
|
+
OpenSSL::ASN1::Sequence => 34, # blue
|
34
|
+
OpenSSL::ASN1::OctetString => 32, # green
|
35
|
+
OpenSSL::ASN1::Integer => 33, # yellow
|
36
|
+
OpenSSL::ASN1::ObjectId => 35, # magenta
|
37
|
+
OpenSSL::ASN1::ASN1Data => 36 # cyan
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
# basic types
|
41
|
+
ASN_COLORS.each_key do |klass|
|
42
|
+
refine(klass) do
|
43
|
+
def to_hex
|
44
|
+
"#{colorize_hex} (#{value.to_s.inspect})"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# composite types
|
50
|
+
refine(OpenSSL::ASN1::Sequence) do
|
51
|
+
def to_hex
|
52
|
+
values = value.map(&:to_der).join
|
53
|
+
hex_values = value.map(&:to_hex).map { |s| s.gsub(/(\t+)/) { "\t#{Regexp.last_match(1)}" } }.map { |s| "\n\t#{s}" }.join
|
54
|
+
der = to_der
|
55
|
+
der = der.sub(values, "")
|
56
|
+
|
57
|
+
"#{colorize_hex(der)}#{hex_values}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
refine(OpenSSL::ASN1::ASN1Data) do
|
62
|
+
attr_reader :label
|
63
|
+
|
64
|
+
def with_label(label)
|
65
|
+
@label = label
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_hex
|
70
|
+
case value
|
71
|
+
when Array
|
72
|
+
values = value.map(&:to_der).join
|
73
|
+
hex_values = value.map(&:to_hex)
|
74
|
+
.map { |s| s.gsub(/(\t+)/) { "\t#{Regexp.last_match(1)}" } }
|
75
|
+
.map { |s| "\n\t#{s}" }.join
|
76
|
+
der = to_der
|
77
|
+
der = der.sub(values, "")
|
78
|
+
else
|
79
|
+
der = to_der
|
80
|
+
hex_values = nil
|
81
|
+
end
|
82
|
+
|
83
|
+
"#{colorize_hex(der)}#{hex_values}"
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def colorize_hex(der = to_der)
|
89
|
+
hex = Hexdump.dump(der, separator: " ")
|
90
|
+
lbl = @label || self.class.name.split("::").last
|
91
|
+
"#{lbl}: \e[#{ASN_COLORS[self.class]}m#{hex}\e[0m"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module Hexdump
|
97
|
+
using StringExtensions
|
98
|
+
|
99
|
+
def self.dump(data, width: 8, in_groups_of: 4, separator: "\n")
|
100
|
+
pairs = data.unpack1("H*").scan(/.{#{in_groups_of}}/)
|
101
|
+
pairs.each_slice(width).map do |row|
|
102
|
+
row.join(" ")
|
103
|
+
end.join(separator)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Like a string, but it prints an hex-string version of itself
|
108
|
+
class HexString < String
|
109
|
+
def inspect
|
110
|
+
Hexdump.dump(self, in_groups_of: 2, separator: " ")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NETSNMP
|
4
|
+
module Loggable
|
5
|
+
DEBUG = ENV.key?("NETSNMP_DEBUG") ? $stderr : nil
|
6
|
+
DEBUG_LEVEL = (ENV["NETSNMP_DEBUG"] || 1).to_i
|
7
|
+
|
8
|
+
def initialize(debug: DEBUG, debug_level: DEBUG_LEVEL, **opts)
|
9
|
+
super(**opts)
|
10
|
+
@debug = debug
|
11
|
+
@debug_level = debug_level
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
COLORS = {
|
17
|
+
black: 30,
|
18
|
+
red: 31,
|
19
|
+
green: 32,
|
20
|
+
yellow: 33,
|
21
|
+
blue: 34,
|
22
|
+
magenta: 35,
|
23
|
+
cyan: 36,
|
24
|
+
white: 37
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def log(level: @debug_level)
|
28
|
+
return unless @debug
|
29
|
+
return unless @debug_level >= level
|
30
|
+
|
31
|
+
debug_stream = @debug
|
32
|
+
|
33
|
+
debug_stream << (+"\n" << yield << "\n")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|