netsnmp 0.1.8 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -27
  3. data/lib/netsnmp.rb +3 -21
  4. data/lib/netsnmp/client.rb +4 -5
  5. data/lib/netsnmp/encryption/aes.rb +1 -3
  6. data/lib/netsnmp/encryption/des.rb +0 -2
  7. data/lib/netsnmp/errors.rb +1 -0
  8. data/lib/netsnmp/extensions.rb +113 -0
  9. data/lib/netsnmp/loggable.rb +36 -0
  10. data/lib/netsnmp/message.rb +70 -28
  11. data/lib/netsnmp/mib.rb +172 -0
  12. data/lib/netsnmp/mib/parser.rb +750 -0
  13. data/lib/netsnmp/oid.rb +7 -12
  14. data/lib/netsnmp/pdu.rb +23 -12
  15. data/lib/netsnmp/scoped_pdu.rb +8 -2
  16. data/lib/netsnmp/security_parameters.rb +22 -14
  17. data/lib/netsnmp/session.rb +14 -16
  18. data/lib/netsnmp/v3_session.rb +21 -9
  19. data/lib/netsnmp/varbind.rb +27 -22
  20. data/lib/netsnmp/version.rb +1 -1
  21. data/sig/client.rbs +24 -0
  22. data/sig/loggable.rbs +16 -0
  23. data/sig/message.rbs +9 -0
  24. data/sig/mib.rbs +21 -0
  25. data/sig/mib/parser.rbs +7 -0
  26. data/sig/netsnmp.rbs +19 -0
  27. data/sig/oid.rbs +18 -0
  28. data/sig/openssl.rbs +20 -0
  29. data/sig/pdu.rbs +48 -0
  30. data/sig/scoped_pdu.rbs +15 -0
  31. data/sig/security_parameters.rbs +58 -0
  32. data/sig/session.rbs +38 -0
  33. data/sig/timeticks.rbs +7 -0
  34. data/sig/v3_session.rbs +21 -0
  35. data/sig/varbind.rbs +30 -0
  36. data/spec/client_spec.rb +26 -8
  37. data/spec/handlers/celluloid_spec.rb +4 -3
  38. data/spec/mib_spec.rb +13 -0
  39. data/spec/session_spec.rb +2 -2
  40. data/spec/spec_helper.rb +9 -5
  41. data/spec/support/request_examples.rb +2 -2
  42. data/spec/v3_session_spec.rb +4 -4
  43. data/spec/varbind_spec.rb +5 -3
  44. metadata +31 -71
  45. data/.coveralls.yml +0 -1
  46. data/.gitignore +0 -14
  47. data/.rspec +0 -2
  48. data/.rubocop.yml +0 -11
  49. data/.rubocop_todo.yml +0 -69
  50. data/.travis.yml +0 -28
  51. data/Gemfile +0 -22
  52. data/Rakefile +0 -30
  53. data/netsnmp.gemspec +0 -31
  54. data/spec/support/Dockerfile +0 -14
  55. data/spec/support/specs.sh +0 -51
  56. data/spec/support/stop_docker.sh +0 -5
@@ -2,41 +2,71 @@
2
2
 
3
3
  module NETSNMP
4
4
  # Factory for the SNMP v3 Message format
5
- module Message
6
- module_function
5
+ class Message
6
+ using ASNExtensions
7
7
 
8
- AUTHNONE = OpenSSL::ASN1::OctetString.new("\x00" * 12)
8
+ prepend Loggable
9
+
10
+ AUTHNONE = OpenSSL::ASN1::OctetString.new("\x00" * 12).with_label(:auth_mask)
9
11
  PRIVNONE = OpenSSL::ASN1::OctetString.new("")
10
- MSG_MAX_SIZE = OpenSSL::ASN1::Integer.new(65507)
11
- MSG_SECURITY_MODEL = OpenSSL::ASN1::Integer.new(3) # usmSecurityModel
12
- MSG_VERSION = OpenSSL::ASN1::Integer.new(3)
12
+ MSG_MAX_SIZE = OpenSSL::ASN1::Integer.new(65507).with_label(:max_message_size)
13
+ MSG_SECURITY_MODEL = OpenSSL::ASN1::Integer.new(3).with_label(:security_model) # usmSecurityModel
14
+ MSG_VERSION = OpenSSL::ASN1::Integer.new(3).with_label(:message_version)
13
15
  MSG_REPORTABLE = 4
14
16
 
17
+ def initialize(**); end
18
+
15
19
  # @param [String] payload of an snmp v3 message which can be decoded
16
20
  # @param [NETSMP::SecurityParameters, #decode] security_parameters knowns how to decode the stream
17
21
  #
18
22
  # @return [NETSNMP::ScopedPDU] the decoded PDU
19
23
  #
20
24
  def decode(stream, security_parameters:)
21
- asn_tree = OpenSSL::ASN1.decode(stream)
22
- _version, _headers, sec_params, pdu_payload = asn_tree.value
25
+ log { "received encoded V3 message" }
26
+ log { Hexdump.dump(stream) }
27
+ asn_tree = OpenSSL::ASN1.decode(stream).with_label(:v3_message)
28
+
29
+ version, headers, sec_params, pdu_payload = asn_tree.value
30
+ version.with_label(:message_version)
31
+ headers.with_label(:headers)
32
+ sec_params.with_label(:security_params)
33
+ pdu_payload.with_label(:pdu)
34
+
35
+ _, _, message_flags, = headers.value
23
36
 
24
- sec_params_asn = OpenSSL::ASN1.decode(sec_params.value).value
37
+ # get last byte
38
+ # discard the left-outermost bits and keep the remaining two
39
+ security_level = message_flags.with_label(:message_flags).value.unpack("C*").last & 3
25
40
 
26
- engine_id, engine_boots, engine_time, _username, auth_param, priv_param = sec_params_asn.map(&:value)
41
+ sec_params_asn = OpenSSL::ASN1.decode(sec_params.value).with_label(:security_params)
42
+
43
+ engine_id, engine_boots, engine_time, username, auth_param, priv_param = sec_params_asn.value
44
+ engine_id.with_label(:engine_id)
45
+ engine_boots.with_label(:engine_boots)
46
+ engine_time.with_label(:engine_time)
47
+ username.with_label(:username)
48
+ auth_param.with_label(:auth_param)
49
+ priv_param.with_label(:priv_param)
50
+
51
+ log(level: 2) { asn_tree.to_hex }
52
+ log(level: 2) { sec_params_asn.to_hex }
27
53
 
28
54
  # validate_authentication
29
- security_parameters.verify(stream.sub(auth_param, AUTHNONE.value), auth_param)
55
+ auth_param = auth_param.value
56
+ security_parameters.verify(stream.sub(auth_param, AUTHNONE.value), auth_param, security_level: security_level)
30
57
 
31
- engine_boots = engine_boots.to_i
32
- engine_time = engine_time.to_i
58
+ engine_boots = engine_boots.value.to_i
59
+ engine_time = engine_time.value.to_i
33
60
 
34
- encoded_pdu = security_parameters.decode(pdu_payload, salt: priv_param,
61
+ encoded_pdu = security_parameters.decode(pdu_payload, salt: priv_param.value,
35
62
  engine_boots: engine_boots,
36
- engine_time: engine_time)
63
+ engine_time: engine_time,
64
+ security_level: security_level)
37
65
 
66
+ log { "received response PDU" }
38
67
  pdu = ScopedPDU.decode(encoded_pdu)
39
- [pdu, engine_id, engine_boots, engine_time]
68
+ log(level: 2) { pdu.to_hex }
69
+ [pdu, engine_id.value, engine_boots, engine_time]
40
70
  end
41
71
 
42
72
  # @param [NETSNMP::ScopedPDU] the PDU to encode in the message
@@ -45,36 +75,48 @@ module NETSNMP
45
75
  # @return [String] the byte representation of an SNMP v3 Message
46
76
  #
47
77
  def encode(pdu, security_parameters:, engine_boots: 0, engine_time: 0)
78
+ log(level: 2) { pdu.to_hex }
79
+ log { "encoding PDU in V3 message..." }
48
80
  scoped_pdu, salt_param = security_parameters.encode(pdu, salt: PRIVNONE,
49
81
  engine_boots: engine_boots,
50
82
  engine_time: engine_time)
51
83
 
52
84
  sec_params = OpenSSL::ASN1::Sequence.new([
53
- OpenSSL::ASN1::OctetString.new(security_parameters.engine_id),
54
- OpenSSL::ASN1::Integer.new(engine_boots),
55
- OpenSSL::ASN1::Integer.new(engine_time),
56
- OpenSSL::ASN1::OctetString.new(security_parameters.username),
85
+ OpenSSL::ASN1::OctetString.new(security_parameters.engine_id).with_label(:engine_id),
86
+ OpenSSL::ASN1::Integer.new(engine_boots).with_label(:engine_boots),
87
+ OpenSSL::ASN1::Integer.new(engine_time).with_label(:engine_time),
88
+ OpenSSL::ASN1::OctetString.new(security_parameters.username).with_label(:username),
57
89
  AUTHNONE,
58
90
  salt_param
59
- ])
91
+ ]).with_label(:security_params)
92
+ log(level: 2) { sec_params.to_hex }
93
+
60
94
  message_flags = MSG_REPORTABLE | security_parameters.security_level
61
- message_id = OpenSSL::ASN1::Integer.new(SecureRandom.random_number(2147483647))
95
+ message_id = OpenSSL::ASN1::Integer.new(SecureRandom.random_number(2147483647)).with_label(:message_id)
62
96
  headers = OpenSSL::ASN1::Sequence.new([
63
- message_id, MSG_MAX_SIZE,
64
- OpenSSL::ASN1::OctetString.new([String(message_flags)].pack("h*")),
97
+ message_id,
98
+ MSG_MAX_SIZE,
99
+ OpenSSL::ASN1::OctetString.new([String(message_flags)].pack("h*")).with_label(:message_flags),
65
100
  MSG_SECURITY_MODEL
66
- ])
101
+ ]).with_label(:headers)
67
102
 
68
103
  encoded = OpenSSL::ASN1::Sequence([
69
104
  MSG_VERSION,
70
105
  headers,
71
- OpenSSL::ASN1::OctetString.new(sec_params.to_der),
106
+ OpenSSL::ASN1::OctetString.new(sec_params.to_der).with_label(:security_params),
72
107
  scoped_pdu
73
- ]).to_der
108
+ ]).with_label(:v3_message)
109
+ log(level: 2) { encoded.to_hex }
110
+
111
+ encoded = encoded.to_der
112
+ log { Hexdump.dump(encoded) }
74
113
  signature = security_parameters.sign(encoded)
75
114
  if signature
76
- auth_salt = OpenSSL::ASN1::OctetString.new(signature)
115
+ log { "signing V3 message..." }
116
+ auth_salt = OpenSSL::ASN1::OctetString.new(signature).with_label(:auth)
117
+ log(level: 2) { auth_salt.to_hex }
77
118
  encoded.sub!(AUTHNONE.to_der, auth_salt.to_der)
119
+ log { Hexdump.dump(encoded) }
78
120
  end
79
121
  encoded
80
122
  end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mib/parser"
4
+
5
+ module NETSNMP
6
+ module MIB
7
+ using IsNumericExtensions
8
+
9
+ OIDREGEX = /^[\d\.]*$/
10
+
11
+ module_function
12
+
13
+ MIBDIRS = ENV.fetch("MIBDIRS", File.join("/usr", "share", "snmp", "mibs")).split(":")
14
+ PARSER = Parser.new
15
+ @parser_mutex = Mutex.new
16
+ @modules_loaded = []
17
+ @object_identifiers = {}
18
+
19
+ # Translates na identifier, such as "sysDescr", into an OID
20
+ def oid(identifier)
21
+ prefix, *suffix = case identifier
22
+ when Array
23
+ identifier
24
+ else
25
+ identifier.split(".", 2)
26
+ end
27
+
28
+ # early exit if it's an OID already
29
+ unless prefix.integer?
30
+ load_defaults
31
+ # load module if need be
32
+ idx = prefix.index("::")
33
+ if idx
34
+ mod = prefix[0..(idx - 1)]
35
+ type = prefix[(idx + 2)..-1]
36
+ unless module_loaded?(mod)
37
+ return unless load(mod)
38
+ end
39
+ else
40
+ type = prefix
41
+ end
42
+
43
+ return if type.nil? || type.empty?
44
+
45
+ prefix = @object_identifiers[type] ||
46
+ raise(Error, "can't convert #{type} to OID")
47
+
48
+ end
49
+
50
+ [prefix, *suffix].join(".")
51
+ end
52
+
53
+ def identifier(oid)
54
+ @object_identifiers.select do |_, ids_oid|
55
+ oid.start_with?(ids_oid)
56
+ end.sort_by(&:size).first
57
+ end
58
+
59
+ #
60
+ # Loads a MIB. Can be called multiple times, as it'll load it once.
61
+ #
62
+ # Accepts the MIB name in several ways:
63
+ #
64
+ # MIB.load("SNMPv2-MIB")
65
+ # MIB.load("SNMPv2-MIB.txt")
66
+ # MIB.load("/path/to/SNMPv2-MIB.txt")
67
+ #
68
+ def load(mod)
69
+ unless File.file?(mod)
70
+ moddir = nil
71
+ MIBDIRS.each do |mibdir|
72
+ if File.exist?(File.join(mibdir, mod))
73
+ moddir = File.join(mibdir, mod)
74
+ break
75
+ elsif File.extname(mod).empty? && File.exist?(File.join(mibdir, "#{mod}.txt"))
76
+ moddir = File.join(mibdir, "#{mod}.txt")
77
+ break
78
+ end
79
+ end
80
+ return false unless moddir
81
+ mod = moddir
82
+ end
83
+ return true if @modules_loaded.include?(mod)
84
+ do_load(mod)
85
+ @modules_loaded << mod
86
+ true
87
+ end
88
+
89
+ def module_loaded?(mod)
90
+ if File.file?(mod)
91
+ @modules_loaded.include?(mod)
92
+ else
93
+ @modules_loaded.map { |path| File.basename(path, ".*") }.include?(mod)
94
+ end
95
+ end
96
+
97
+ TYPES = ["OBJECT IDENTIFIER", "OBJECT-TYPE", "MODULE-IDENTITY"].freeze
98
+
99
+ STATIC_MIB_TO_OID = {
100
+ "iso" => "1"
101
+ }.freeze
102
+
103
+ #
104
+ # Loads the MIB all the time, where +mod+ is the absolute path to the MIB.
105
+ #
106
+ def do_load(mod)
107
+ data = @parser_mutex.synchronize { PARSER.parse(File.read(mod)) }
108
+
109
+ imports = load_imports(data[:imports])
110
+
111
+ declarations = Hash[
112
+ data[:declarations].reject { |dec| !dec.key?(:name) || !TYPES.include?(dec[:type]) }
113
+ .map { |dec| [String(dec[:name]), String(dec[:value]).split(/ +/)] }
114
+ ]
115
+
116
+ declarations.each do |nme, value|
117
+ store_oid_in_identifiers(nme, value, imports: imports, declarations: declarations)
118
+ end
119
+ end
120
+
121
+ def store_oid_in_identifiers(nme, value, imports:, declarations:)
122
+ oid = value.flat_map do |cp|
123
+ if cp.integer?
124
+ cp
125
+ elsif @object_identifiers.key?(cp)
126
+ @object_identifiers[cp]
127
+ elsif declarations.key?(cp)
128
+ store_oid_in_identifiers(cp, declarations[cp], imports: imports, declarations: declarations)
129
+ @object_identifiers[cp]
130
+ else
131
+ STATIC_MIB_TO_OID[cp] || begin
132
+ imported_mod, = imports.find do |_, identifiers|
133
+ identifiers.include?(cp)
134
+ end
135
+
136
+ raise Error, "didn't find a module to import \"#{cp}\" from" unless imported_mod
137
+
138
+ load(imported_mod)
139
+
140
+ @object_identifiers[cp]
141
+ end
142
+ end
143
+ end.join(".")
144
+
145
+ @object_identifiers[nme] = oid
146
+ end
147
+
148
+ #
149
+ # Reformats the import lists into an hash indexed by module name, to a list of
150
+ # imported names
151
+ #
152
+ def load_imports(imports)
153
+ return unless imports
154
+
155
+ imports = [imports] unless imports.respond_to?(:to_ary)
156
+ imports.each_with_object({}) do |import, imp|
157
+ imp[String(import[:name])] = case import[:ids]
158
+ when Hash
159
+ [String(import[:ids][:name])]
160
+ else
161
+ import[:ids].map { |id| String(id[:name]) }
162
+ end
163
+ end
164
+ end
165
+
166
+ def load_defaults
167
+ # loading the defaults MIBS
168
+ load("SNMPv2-MIB")
169
+ load("IF-MIB")
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,750 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parslet"
4
+
5
+ module NETSNMP::MIB
6
+ class Parser < Parslet::Parser
7
+ root :mibfile
8
+
9
+ def spaced(character = nil)
10
+ if character.nil? && block_given?
11
+ yield >> space.repeat
12
+ else
13
+ str(character) >> space.repeat
14
+ end
15
+ end
16
+
17
+ def curly(atom)
18
+ str("{") >> space.repeat >> atom >> space.repeat >> str("}")
19
+ end
20
+
21
+ def bracketed(atom)
22
+ str("(") >> space.repeat >> atom >> space.repeat >> str(")")
23
+ end
24
+
25
+ def square_bracketed(atom)
26
+ str("[") >> space.repeat >> atom >> space.repeat >> str("]")
27
+ end
28
+
29
+ def with_separator(atom, separator = nil)
30
+ if separator
31
+ sep = if separator.is_a?(String)
32
+ space.repeat >> str(separator) >> space.repeat
33
+ else
34
+ separator
35
+ end
36
+
37
+ atom >> (sep >> atom).repeat
38
+ else
39
+ atom >> (space.repeat >> atom).repeat
40
+ end
41
+ end
42
+
43
+ rule(:mibfile) do
44
+ space.repeat >> modules.maybe
45
+ end
46
+
47
+ rule(:modules) do
48
+ with_separator(mod)
49
+ end
50
+
51
+ rule(:mod) do
52
+ spaced { module_name.as(:name) } >> module_oid >>
53
+ spaced("DEFINITIONS") >> colon_colon_part >>
54
+ spaced("BEGIN") >>
55
+ exports_part.as(:exports) >>
56
+ linkage_part.as(:imports) >>
57
+ declaration_part.as(:declarations) >>
58
+ spaced("END")
59
+ end
60
+
61
+ rule(:module_name) { uppercase_identifier }
62
+
63
+ rule(:module_oid) do
64
+ spaced { curly(object_identifier) }.maybe
65
+ end
66
+
67
+ rule(:declaration_part) do
68
+ spaced { declarations }.maybe
69
+ end
70
+
71
+ rule(:exports_part) do
72
+ spaced { exports_clause }.maybe
73
+ end
74
+
75
+ rule(:exports_clause) do
76
+ spaced("EXPORTS") >> spaced { import_identifiers } >> str(";")
77
+ end
78
+
79
+ rule(:linkage_part) do
80
+ spaced { linkage_clause }.maybe
81
+ end
82
+
83
+ rule(:linkage_clause) do
84
+ spaced("IMPORTS") >> spaced { import_part } >> str(";")
85
+ end
86
+
87
+ rule(:import_part) do
88
+ imports.maybe
89
+ end
90
+
91
+ rule(:imports) { with_separator(import) }
92
+
93
+ rule(:import) do
94
+ spaced { import_identifiers.as(:ids) } >> spaced("FROM") >> module_name.as(:name)
95
+ end
96
+
97
+ rule(:import_identifiers) { with_separator(import_identifier.as(:name), ",") }
98
+
99
+ rule(:import_identifier) do
100
+ lowercase_identifier | uppercase_identifier | imported_keyword
101
+ end
102
+
103
+ rule(:imported_keyword) do
104
+ imported_smi_keyword |
105
+ str("BITS") |
106
+ str("Integer32") |
107
+ str("IpAddress") |
108
+ str("MANDATORY-GROUPS") |
109
+ str("MODULE-COMPLIANCE") |
110
+ str("MODULE-IDENTITY") |
111
+ str("OBJECT-GROUP") |
112
+ str("OBJECT-IDENTITY") |
113
+ str("OBJECT-TYPE") |
114
+ str("Opaque") |
115
+ str("TEXTUAL-CONVENTION") |
116
+ str("TimeTicks") |
117
+ str("Unsigned32")
118
+ end
119
+
120
+ rule(:imported_smi_keyword) do
121
+ str("AGENT-CAPABILITIES") |
122
+ str("Counter32") |
123
+ str("Counter64") |
124
+ str("Gauge32") |
125
+ str("NOTIFICATION-GROUP") |
126
+ str("NOTIFICATION-TYPE") |
127
+ str("TRAP-TYPE")
128
+ end
129
+
130
+ rule(:declarations) do
131
+ with_separator(declaration)
132
+ end
133
+
134
+ rule(:declaration) do
135
+ type_declaration |
136
+ value_declaration |
137
+ object_identity_clause |
138
+ object_type_clause |
139
+ traptype_clause |
140
+ notification_type_clause |
141
+ module_identity_clause |
142
+ module_compliance_clause |
143
+ object_group_clause |
144
+ notification_group_clause |
145
+ agent_capabilities_clause |
146
+ macro_clause
147
+ end
148
+
149
+ rule(:macro_clause) do
150
+ spaced { macro_name.as(:name) } >> spaced { str("MACRO").as(:type) } >> colon_colon_part >>
151
+ spaced("BEGIN") >>
152
+ # ignoring macro clauses
153
+ match("^(?!END)").repeat >>
154
+ spaced("END")
155
+ end
156
+
157
+ rule(:macro_name) do
158
+ str("MODULE-IDENTITY") |
159
+ str("OBJECT-TYPE") |
160
+ str("TRAP-TYPE") |
161
+ str("NOTIFICATION-TYPE") |
162
+ str("OBJECT-IDENTITY") |
163
+ str("TEXTUAL-CONVENTION") |
164
+ str("OBJECT-GROUP") |
165
+ str("NOTIFICATION-GROUP") |
166
+ str("MODULE-COMPLIANCE") |
167
+ str("AGENT-CAPABILITIES")
168
+ end
169
+
170
+ rule(:agent_capabilities_clause) do
171
+ spaced { lowercase_identifier.as(:name) } >>
172
+ spaced { str("AGENT-CAPABILITIES").as(:type) } >>
173
+ spaced("PRODUCT-RELEASE") >> spaced { text } >>
174
+ spaced("STATUS") >> spaced { status } >>
175
+ spaced("DESCRIPTION") >> spaced { text } >>
176
+ spaced { refer_part }.maybe >>
177
+ spaced { module_part_capabilities }.maybe >>
178
+ colon_colon_part >> curly(object_identifier)
179
+ end
180
+
181
+ rule(:module_part_capabilities) do
182
+ modules_capabilities
183
+ end
184
+
185
+ rule(:modules_capabilities) do
186
+ with_separator(module_capabilities)
187
+ end
188
+
189
+ rule(:module_capabilities) do
190
+ spaced("SUPPORTS") >>
191
+ module_name_capabilities >>
192
+ spaced("INCLUDES") >> curly(capabilities_groups) >>
193
+ spaced { variation_part }.maybe
194
+ end
195
+
196
+ rule(:module_name_capabilities) do
197
+ spaced { uppercase_identifier } >> object_identifier | uppercase_identifier
198
+ end
199
+
200
+ rule(:capabilities_groups) do
201
+ with_separator(capabilities_group)
202
+ end
203
+
204
+ rule(:capabilities_group) { objectIdentifier }
205
+
206
+ rule(:variation_part) do
207
+ variations
208
+ end
209
+
210
+ rule(:variations) do
211
+ with_separator(variation)
212
+ end
213
+
214
+ rule(:variation) do
215
+ spaced("VARIATION") >> object_identifier >>
216
+ spaced { syntax_part }.maybe >>
217
+ spaced { write_syntax_part } >>
218
+ spaced { variation_access_part }.maybe >>
219
+ spaced { creation_part }.maybe >>
220
+ spaced { def_val_part }.maybe >>
221
+ spaced("DESCRIPTION") >> text
222
+ end
223
+
224
+ rule(:variation_access_part) do
225
+ spaced("ACCESS") >> variation_access
226
+ end
227
+
228
+ rule(:variation_access) { lowercase_identifier }
229
+
230
+ rule(:creation_part) do
231
+ spaced("CREATION-REQUIRES") >> curly(cells)
232
+ end
233
+
234
+ rule(:cells) { with_separator(cell, ",") }
235
+
236
+ rule(:cell) { object_identifier }
237
+
238
+ rule(:notification_group_clause) do
239
+ spaced { lowercase_identifier.as(:name) } >>
240
+ spaced { str("NOTIFICATION-GROUP").as(:type) } >>
241
+ spaced { notifications_part } >>
242
+ spaced("STATUS") >> spaced { status } >>
243
+ spaced("DESCRIPTION") >> spaced { text } >>
244
+ spaced { refer_part }.maybe >>
245
+ colon_colon_part >> curly(object_identifier)
246
+ end
247
+
248
+ rule(:notifications_part) do
249
+ spaced("NOTIFICATIONS") >> curly(notifications)
250
+ end
251
+
252
+ rule(:notifications) do
253
+ with_separator(notification, ",")
254
+ end
255
+
256
+ rule(:notification) do
257
+ notification_name
258
+ end
259
+
260
+ rule(:object_group_clause) do
261
+ spaced { lowercase_identifier.as(:name) } >>
262
+ spaced { str("OBJECT-GROUP").as(:type) } >>
263
+ spaced { object_group_objects_part } >>
264
+ spaced("STATUS") >> spaced { status } >>
265
+ spaced("DESCRIPTION") >> spaced { text } >>
266
+ spaced { refer_part }.maybe >>
267
+ colon_colon_part >> curly(object_identifier)
268
+ end
269
+
270
+ rule(:object_group_objects_part) do
271
+ spaced("OBJECTS") >> curly(objects)
272
+ end
273
+
274
+ rule(:module_compliance_clause) do
275
+ spaced { lowercase_identifier.as(:name) } >>
276
+ spaced { str("MODULE-COMPLIANCE").as(:type) } >>
277
+ spaced("STATUS") >> spaced { status } >>
278
+ spaced("DESCRIPTION") >> spaced { text } >>
279
+ spaced { refer_part }.maybe >>
280
+ spaced { compliance_modules } >>
281
+ colon_colon_part >> curly(object_identifier)
282
+ end
283
+
284
+ rule(:compliance_modules) do
285
+ with_separator(compliance_module)
286
+ end
287
+
288
+ rule(:compliance_module) do
289
+ spaced { str("MODULE") >> (space_in_line.repeat(1) >> compliance_module_name).maybe } >>
290
+ spaced { mandatory_part }.maybe >>
291
+ compliances.maybe
292
+ end
293
+
294
+ rule(:compliance_module_name) do
295
+ uppercase_identifier
296
+ end
297
+
298
+ rule(:mandatory_part) do
299
+ spaced("MANDATORY-GROUPS") >> curly(mandatory_groups)
300
+ end
301
+
302
+ rule(:compliances) do
303
+ with_separator(compliance).as(:compliances)
304
+ end
305
+
306
+ rule(:compliance) do
307
+ compliance_group | compliance_object
308
+ end
309
+
310
+ rule(:compliance_group) do
311
+ spaced { str("GROUP").as(:type) } >> spaced { object_identifier.as(:name) } >>
312
+ spaced("DESCRIPTION") >> text
313
+ end
314
+
315
+ rule(:compliance_object) do
316
+ spaced { str("OBJECT").as(:type) } >>
317
+ spaced { object_identifier.as(:name) } >>
318
+ spaced { syntax_part }.maybe >>
319
+ spaced { write_syntax_part }.maybe >>
320
+ spaced { access_part }.maybe >>
321
+ spaced("DESCRIPTION") >> text
322
+ end
323
+
324
+ rule(:syntax_part) do
325
+ spaced("SYNTAX") >> syntax.as(:syntax)
326
+ end
327
+
328
+ rule(:write_syntax_part) do
329
+ (spaced("WRITE-SYNTAX") >> spaced { syntax }).maybe
330
+ end
331
+
332
+ rule(:access_part) do
333
+ (spaced("MIN-ACCESS") >> spaced { access }).maybe
334
+ end
335
+
336
+ rule(:mandatory_groups) do
337
+ with_separator(mandatory_group, ",").as(:groups)
338
+ end
339
+
340
+ rule(:mandatory_group) { object_identifier.as(:name) }
341
+
342
+ rule(:module_identity_clause) do
343
+ spaced { lowercase_identifier.as(:name) } >>
344
+ spaced { str("MODULE-IDENTITY").as(:type) } >>
345
+ (spaced("SUBJECT-CATEGORIES") >> curly(category_ids)).maybe >>
346
+ spaced("LAST-UPDATED") >> spaced { ext_utc_time } >>
347
+ spaced("ORGANIZATION") >> spaced { text.as(:organization) } >>
348
+ spaced("CONTACT-INFO") >> spaced { text.as(:contact_info) } >>
349
+ spaced("DESCRIPTION") >> spaced { text } >>
350
+ spaced { revisions }.maybe >>
351
+ colon_colon_part >> curly(object_identifier.as(:value))
352
+ end
353
+
354
+ rule(:ext_utc_time) { text }
355
+
356
+ rule(:revisions) do
357
+ with_separator(revision)
358
+ end
359
+
360
+ rule(:revision) do
361
+ spaced("REVISION") >> spaced { ext_utc_time } >>
362
+ spaced("DESCRIPTION") >>
363
+ text
364
+ end
365
+
366
+ rule(:category_ids) do
367
+ with_separator(category_id, ",")
368
+ end
369
+
370
+ rule(:category_id) do
371
+ spaced { lowercase_identifier } >> bracketed(number) | lowercase_identifier
372
+ end
373
+
374
+ rule(:notification_type_clause) do
375
+ spaced { lowercase_identifier.as(:name) } >>
376
+ spaced { str("NOTIFICATION-TYPE").as(:type) } >>
377
+ spaced { notification_objects_part }.maybe >>
378
+ spaced("STATUS") >> spaced { status } >>
379
+ spaced("DESCRIPTION") >> spaced { text } >>
380
+ spaced { refer_part }.maybe >>
381
+ colon_colon_part >> curly(notification_name)
382
+ end
383
+
384
+ rule(:notification_objects_part) do
385
+ spaced("OBJECTS") >> curly(objects)
386
+ end
387
+
388
+ rule(:objects) do
389
+ with_separator(object, ",")
390
+ end
391
+
392
+ rule(:object) do
393
+ object_identifier
394
+ end
395
+
396
+ rule(:notification_name) do
397
+ object_identifier
398
+ end
399
+
400
+ rule(:traptype_clause) do
401
+ spaced { fuzzy_lowercase_identifier.as(:name) } >>
402
+ spaced { str("TRAP-TYPE").as(:type) } >> spaced { enterprise_part } >>
403
+ spaced { var_part }.maybe >>
404
+ spaced { descr_part }.maybe >>
405
+ spaced { refer_part }.maybe >>
406
+ colon_colon_part >> number
407
+ end
408
+
409
+ rule(:enterprise_part) do
410
+ spaced("ENTERPRISE") >> object_identifier |
411
+ spaced("ENTERPRISE") >> curly(object_identifier)
412
+ end
413
+
414
+ rule(:var_part) do
415
+ spaced("VARIABLES") >> curly(var_types)
416
+ end
417
+
418
+ rule(:var_types) do
419
+ with_separator(var_type, ",")
420
+ end
421
+
422
+ rule(:var_type) { object_identifier }
423
+
424
+ rule(:descr_part) do
425
+ spaced("DESCRIPTION") >> text
426
+ end
427
+
428
+ rule(:object_type_clause) do
429
+ spaced { lowercase_identifier.as(:name) } >>
430
+ spaced { str("OBJECT-TYPE").as(:type) } >>
431
+ spaced { syntax_part }.maybe >>
432
+ spaced { units_part }.maybe >>
433
+ spaced { max_access_part }.maybe >>
434
+ (spaced("STATUS") >> spaced { status }).maybe >>
435
+ spaced { description_clause }.maybe >>
436
+ spaced { refer_part }.maybe >>
437
+ spaced { index_part }.maybe >>
438
+ spaced { mib_index }.maybe >>
439
+ spaced { def_val_part }.maybe >>
440
+ colon_colon_part >> curly(object_identifier.as(:value))
441
+ end
442
+
443
+ rule(:object_identity_clause) do
444
+ spaced { lowercase_identifier.as(:name) } >>
445
+ spaced { str("OBJECT-IDENTITY").as(:type) } >>
446
+ spaced("STATUS") >> spaced { status } >>
447
+ spaced("DESCRIPTION") >> spaced { text } >>
448
+ spaced { refer_part }.maybe >>
449
+ colon_colon_part >> curly(object_identifier)
450
+ end
451
+
452
+ rule(:units_part) do
453
+ spaced("UNITS") >> text.as(:units)
454
+ end
455
+
456
+ rule(:max_access_part) do
457
+ spaced("MAX-ACCESS") >> access | spaced("ACCESS") >> access
458
+ end
459
+
460
+ rule(:access) { lowercase_identifier }
461
+
462
+ rule(:description_clause) do
463
+ spaced("DESCRIPTION") >> text
464
+ end
465
+
466
+ rule(:index_part) do
467
+ spaced("AUGMENTS") >> curly(entry)
468
+ end
469
+
470
+ rule(:mib_index) do
471
+ spaced("INDEX") >> curly(index_types)
472
+ end
473
+
474
+ rule(:def_val_part) do
475
+ spaced("DEFVAL") >> curly(valueof_simple_syntax)
476
+ end
477
+
478
+ rule(:valueof_simple_syntax) do
479
+ value | lowercase_identifier | text | curly(object_identifiers_defval)
480
+ end
481
+
482
+ rule(:object_identifiers_defval) do
483
+ with_separator(object_identifier_defval)
484
+ end
485
+
486
+ rule(:object_identifier_defval) do
487
+ spaced { lowercase_identifier } >> bracketed(number) |
488
+ number
489
+ end
490
+
491
+ rule(:index_types) do
492
+ with_separator(index_type, ",")
493
+ end
494
+
495
+ rule(:index_type) do
496
+ spaced("IMPLIED") >> idx | idx
497
+ end
498
+
499
+ rule(:idx) do
500
+ object_identifier
501
+ end
502
+
503
+ rule(:entry) do
504
+ object_identifier
505
+ end
506
+
507
+ rule(:value_declaration) do
508
+ spaced { fuzzy_lowercase_identifier.as(:name) } >>
509
+ spaced { str("OBJECT IDENTIFIER").as(:type) } >>
510
+ colon_colon_part >> curly(object_identifier.as(:value))
511
+ end
512
+
513
+ rule(:fuzzy_lowercase_identifier) do
514
+ lowercase_identifier | uppercase_identifier
515
+ end
516
+
517
+ rule(:object_identifier) do
518
+ sub_identifiers
519
+ end
520
+
521
+ rule(:sub_identifiers) do
522
+ with_separator(sub_identifier, space_in_line.repeat)
523
+ end
524
+
525
+ rule(:sub_identifier) do
526
+ fuzzy_lowercase_identifier |
527
+ number |
528
+ spaced { lowercase_identifier } >> bracketed(number)
529
+ end
530
+
531
+ rule(:type_declaration) do
532
+ spaced { type_name.as(:vartype) } >> colon_colon_part >> type_declaration_rhs
533
+ end
534
+
535
+ rule(:type_name) do
536
+ uppercase_identifier | type_smi
537
+ end
538
+
539
+ rule(:type_smi) do
540
+ type_smi_and_sppi | type_smi_only
541
+ end
542
+
543
+ rule(:type_declaration_rhs) do
544
+ spaced { choice_clause } |
545
+ spaced { str("TEXTUAL-CONVENTION") } >>
546
+ spaced { display_part }.maybe >>
547
+ spaced("STATUS") >> spaced { status } >>
548
+ spaced("DESCRIPTION") >> spaced { text } >>
549
+ spaced { refer_part }.maybe >>
550
+ spaced("SYNTAX") >> syntax |
551
+ syntax
552
+ end
553
+
554
+ rule(:refer_part) do
555
+ spaced("REFERENCE") >> text
556
+ end
557
+
558
+ rule(:choice_clause) do
559
+ # Ignoring choice syntax
560
+ spaced { str("CHOICE").as(:type) } >> curly(match("[^\}]").repeat)
561
+ end
562
+
563
+ rule(:syntax) do
564
+ object_syntax | spaced("BITS").as(:type) >> curly(named_bits)
565
+ end
566
+
567
+ rule(:display_part) do
568
+ spaced("DISPLAY-HINT") >> text
569
+ end
570
+
571
+ rule(:named_bits) do
572
+ with_separator(named_bit, ",")
573
+ end
574
+
575
+ rule(:named_bit) do
576
+ spaced { lowercase_identifier } >> bracketed(number)
577
+ end
578
+
579
+ rule(:object_syntax) do
580
+ conceptual_table |
581
+ entry_type |
582
+ simple_syntax |
583
+ application_syntax |
584
+ type_tag >> simple_syntax |
585
+ row.as(:value)
586
+ end
587
+
588
+ rule(:simple_syntax) do
589
+ spaced { str("INTEGER").as(:type) } >> (integer_subtype | enum_spec).maybe |
590
+ spaced { str("Integer32").as(:type) >> space } >> integer_subtype.maybe |
591
+ spaced { str("OCTET STRING").as(:type) } >> octetstring_subtype.maybe |
592
+ spaced { str("OBJECT IDENTIFIER").as(:type) } >> any_subtype |
593
+ spaced { uppercase_identifier.as(:type) } >> (integer_subtype | enum_spec | octetstring_subtype)
594
+ end
595
+
596
+ rule(:application_syntax) do
597
+ spaced { str("IpAddress").as(:type) >> space } >> any_subtype |
598
+ spaced { str("NetworkAddress").as(:type) >> space } >> any_subtype |
599
+ spaced { str("Counter32").as(:type) >> space } >> integer_subtype.maybe |
600
+ spaced { str("Gauge32").as(:type) >> space } >> integer_subtype.maybe |
601
+ spaced { str("Unsigned32").as(:type) >> space } >> integer_subtype.maybe |
602
+ spaced { str("TimeTicks").as(:type) >> space } >> any_subtype |
603
+ spaced { str("Opaque").as(:type) >> space } >> octetstring_subtype.maybe |
604
+ spaced { str("Counter64").as(:type) >> space } >> integer_subtype.maybe
605
+ end
606
+
607
+ rule(:conceptual_table) do
608
+ spaced { str("SEQUENCE OF").as(:type) } >> row.as(:value)
609
+ end
610
+
611
+ rule(:entry_type) do
612
+ spaced { str("SEQUENCE").as(:type) } >> curly(sequence_items)
613
+ end
614
+
615
+ rule(:type_tag) do
616
+ spaced { square_bracketed(spaced("APPLICATION") >> number.as(:application_type)) } >> spaced("IMPLICIT") |
617
+ spaced { square_bracketed(spaced("UNIVERSAL") >> number.as(:universal_type)) } >> spaced("IMPLICIT")
618
+ end
619
+
620
+ rule(:sequence_items) do
621
+ with_separator(sequence_item, ",")
622
+ end
623
+
624
+ rule(:sequence_item) do
625
+ spaced { lowercase_identifier } >> spaced { sequence_syntax }
626
+ end
627
+
628
+ rule(:sequence_syntax) do
629
+ str("BITS") |
630
+ sequence_object_syntax |
631
+ spaced { uppercase_identifier } >> any_subtype
632
+ end
633
+
634
+ rule(:sequence_object_syntax) do
635
+ sequence_simple_syntax | sequence_application_syntax
636
+ end
637
+
638
+ rule(:sequence_simple_syntax) do
639
+ spaced("INTEGER") >> any_subtype |
640
+ spaced("Integer32") >> any_subtype |
641
+ spaced("OCTET STRING") >> any_subtype |
642
+ spaced("OBJECT IDENTIFIER") >> any_subtype
643
+ end
644
+
645
+ rule(:sequence_application_syntax) do
646
+ spaced { str("IpAddress") >> space } >> any_subtype |
647
+ spaced { str("COUNTER32") } >> any_subtype |
648
+ spaced { str("Gauge32") >> space } >> any_subtype |
649
+ spaced { str("Unsigned32") >> space } >> any_subtype |
650
+ spaced { str("TimeTicks") >> space } >> any_subtype |
651
+ str("Opaque") |
652
+ spaced { str("Counter64") >> space } >> any_subtype
653
+ end
654
+
655
+ rule(:row) { uppercase_identifier }
656
+
657
+ rule(:integer_subtype) { bracketed(ranges) }
658
+
659
+ rule(:octetstring_subtype) do
660
+ bracketed(spaced("SIZE") >> bracketed(ranges))
661
+ end
662
+
663
+ rule(:any_subtype) do
664
+ (integer_subtype | octetstring_subtype | enum_spec).maybe
665
+ end
666
+
667
+ rule(:enum_spec) { curly(enum_items) }
668
+
669
+ rule(:enum_items) do
670
+ with_separator(enum_item.as(:enum), ",")
671
+ end
672
+
673
+ rule(:enum_item) do
674
+ fuzzy_lowercase_identifier.as(:name) >> space.repeat >> bracketed(number.as(:value))
675
+ end
676
+
677
+ rule(:ranges) do
678
+ with_separator(range.as(:range), "|")
679
+ end
680
+
681
+ rule(:range) do
682
+ value.as(:min) >> space.repeat >> (str("..") >> space.repeat >> value.as(:max)).maybe
683
+ end
684
+
685
+ rule(:value) do
686
+ number | hexstring | binstring
687
+ end
688
+
689
+ rule(:status) { lowercase_identifier }
690
+
691
+ rule(:uppercase_identifier) do
692
+ match("[A-Z]") >> match("[A-Za-z0-9\-]").repeat
693
+ end
694
+
695
+ rule(:lowercase_identifier) do
696
+ match("[a-z]") >> match("[A-Za-z0-9\-]").repeat
697
+ end
698
+
699
+ rule(:type_smi_and_sppi) do
700
+ str("IpAddress") | str("TimeTicks") | str("Opaque") | str("Integer32") | str("Unsigned32")
701
+ end
702
+
703
+ rule(:type_smi_only) do
704
+ str("Counter") | str("Gauge32") | str("Counter64")
705
+ end
706
+
707
+ rule(:colon_colon_part) { spaced("::=") }
708
+
709
+ rule(:space_in_line) { match('[ \t]').repeat(1) }
710
+ rule(:cr) { match("\n") }
711
+ rule(:space) do
712
+ # this rule match all not important text
713
+ (match('[ \t\r\n]') | comment_line).repeat(1)
714
+ end
715
+
716
+ rule(:comment_line) do
717
+ (match('\-\-') >> match('[^\n]').repeat >> match('\n'))
718
+ end
719
+
720
+ rule(:space?) { space.maybe }
721
+ rule(:digit) { match["0-9"] }
722
+ rule(:hexchar) { match("[0-9a-fA-F]") }
723
+ rule(:empty) { str("") }
724
+ rule(:number) do
725
+ (
726
+ str("-").maybe >> (
727
+ str("0") | (match("[1-9]") >> digit.repeat)
728
+ ) >> (
729
+ str(".") >> digit.repeat(1)
730
+ ).maybe >> (
731
+ match("[eE]") >> (str("+") | str("-")).maybe >> digit.repeat(1)
732
+ ).maybe
733
+ ).repeat(1)
734
+ end
735
+
736
+ rule(:hexstring) do
737
+ str("'") >> hexchar.repeat >> str("'") >> match("[hH]")
738
+ end
739
+
740
+ rule(:binstring) do
741
+ str("'") >> match["0-1"].repeat >> str("'")
742
+ end
743
+
744
+ rule(:text) do
745
+ str('"') >> (
746
+ str('\\') >> any | str('"').absent? >> any
747
+ ).repeat >> str('"')
748
+ end
749
+ end
750
+ end