ios_parser 0.3.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.
data/lib/ios_parser.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+
3
+ module IOSParser
4
+ def self.lexer
5
+ if const_defined?(:PureLexer)
6
+ PureLexer
7
+ else
8
+ require_relative 'ios_parser/c_lexer'
9
+ CLexer
10
+ end
11
+ end
12
+
13
+ Lexer = lexer
14
+ end
15
+
16
+ require_relative 'ios_parser/ios'
17
+
18
+ module IOSParser
19
+ class << self
20
+ def parse(input)
21
+ IOSParser::IOS.new.call(input)
22
+ end
23
+
24
+ def hash_to_ios(hash)
25
+ IOSParser::IOS::Document.from_hash(hash)
26
+ end
27
+
28
+ def json_to_ios(text)
29
+ hash_to_ios JSON.load(text)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,153 @@
1
+ require_relative '../../../spec_helper'
2
+ require 'ios_parser'
3
+
4
+ module IOSParser
5
+ class IOS
6
+ describe Queryable do
7
+ let(:input) { <<-END }
8
+ policy-map mypolicy_in
9
+ class some_service
10
+ police 300000000 1000000 exceed-action policed-dscp-transmit
11
+ set dscp cs1
12
+ class my_service
13
+ police 600000000 1000000 exceed-action policed-dscp-transmit
14
+ set dscp cs2
15
+ command_with_no_args
16
+ END
17
+
18
+ let(:expectation) { 'set dscp cs1' }
19
+ let(:parsed) { IOSParser.parse(input) }
20
+ subject { parsed.find(matcher).line }
21
+
22
+ describe '#find' do
23
+ context 'shortcut matcher' do
24
+ describe String do
25
+ let(:matcher) { 'set dscp cs1' }
26
+ it { should == expectation }
27
+ end
28
+
29
+ describe Regexp do
30
+ let(:matcher) { /set .* cs1/ }
31
+ it { should == expectation }
32
+ end
33
+
34
+ describe Proc do
35
+ let(:expectation) { 'command_with_no_args' }
36
+ let(:matcher) { ->(c) { c.args.count == 1 } }
37
+ it { should == expectation }
38
+ end
39
+ end # context 'shortcut matcher' do
40
+
41
+ context 'explicit matcher form of shortcut matcher' do
42
+ describe String do
43
+ let(:matcher) { { starts_with: 'set dscp cs1' } }
44
+ it { should == expectation }
45
+ end
46
+
47
+ describe Regexp do
48
+ let(:matcher) { { line: /set .* cs1/ } }
49
+ it { should == expectation }
50
+ end
51
+
52
+ describe Proc do
53
+ let(:expectation) { 'command_with_no_args' }
54
+ let(:matcher) { { procedure: ->(c) { c.args.count == 1 } } }
55
+ it { should == expectation }
56
+ end
57
+ end # context 'explicit matcher form of shortcut matcher' do
58
+
59
+ context 'matcher: contains' do
60
+ describe String do
61
+ let(:matcher) { { contains: 'dscp cs1' } }
62
+ it { should == expectation }
63
+ end
64
+
65
+ describe Array do
66
+ let(:matcher) { { contains: %w(dscp cs1) } }
67
+ it { should == expectation }
68
+ end
69
+ end # context 'matcher: contains' do
70
+
71
+ context 'matcher: ends_with' do
72
+ let(:expectation) { 'class my_service' }
73
+
74
+ describe String do
75
+ let(:matcher) { { ends_with: 'my_service' } }
76
+ it { should == expectation }
77
+ end
78
+
79
+ describe Array do
80
+ let(:matcher) { { ends_with: ['my_service'] } }
81
+ it { should == expectation }
82
+ end
83
+ end # context 'matcher: ends_with' do
84
+
85
+ context 'matcher: all' do
86
+ let(:matcher) { { all: ['set', /cs1/] } }
87
+ it { should == expectation }
88
+ end
89
+
90
+ context 'matcher: parent' do
91
+ let(:matcher) { { parent: /police 3/ } }
92
+ it { should == expectation }
93
+ end
94
+
95
+ context 'matcher: any' do
96
+ let(:matcher) { { any: [/asdf/, /cs1/, /qwerwqe/] } }
97
+ it { should == expectation }
98
+ end
99
+
100
+ context 'matcher: any (with a hash)' do
101
+ let(:matcher) do
102
+ { any: { depth: 0, procedure: ->(c) { c.args.count == 1 } } }
103
+ end
104
+
105
+ it do
106
+ expect(parsed.find_all(matcher).map(&:line))
107
+ .to eq ['policy-map mypolicy_in', 'command_with_no_args']
108
+ end
109
+ end
110
+
111
+ context 'matcher: depth' do
112
+ let(:matcher) { { depth: 3 } }
113
+ it { should == expectation }
114
+ end
115
+
116
+ context 'matcher: none' do
117
+ let(:matcher) do
118
+ { none: [/policy/, /class/, /police/] }
119
+ end
120
+ it { should == expectation }
121
+ end
122
+
123
+ context 'matcher: not_all' do
124
+ let(:matcher) do
125
+ {
126
+ all: {
127
+ none: /policy/,
128
+ not: /class/,
129
+ not_all: /police/
130
+ }
131
+ }
132
+ end
133
+
134
+ it do
135
+ expect(parsed.find(not_all: [/policy/, /class/]).line)
136
+ .to eq 'policy-map mypolicy_in'
137
+ end
138
+ it { should == expectation }
139
+ end
140
+
141
+ context 'matcher: any_child' do
142
+ let(:matcher) { { not: { any_child: /dscp/ } } }
143
+ it { should == expectation }
144
+ end
145
+
146
+ context 'matcher: no_child' do
147
+ let(:matcher) { { no_child: /dscp/ } }
148
+ it { should == expectation }
149
+ end
150
+ end # describe '#find' do
151
+ end # describe Queryable
152
+ end # class IOS
153
+ end # module IOSParser
@@ -0,0 +1,339 @@
1
+ require_relative '../../spec_helper'
2
+ require 'ios_parser'
3
+ require 'ios_parser/lexer'
4
+
5
+ module IOSParser
6
+ describe IOS do
7
+ context 'indented region' do
8
+ let(:input) { <<-END }
9
+ policy-map mypolicy_in
10
+ class myservice_service
11
+ police 300000000 1000000 exceed-action policed-dscp-transmit
12
+ set dscp cs1
13
+ class other_service
14
+ police 600000000 1000000 exceed-action policed-dscp-transmit
15
+ set dscp cs2
16
+ command_with_no_args
17
+ END
18
+
19
+ let(:output) do
20
+ {
21
+ commands:
22
+ [{ args: ['policy-map', 'mypolicy_in'],
23
+ commands:
24
+ [{ args: %w(class myservice_service),
25
+ commands: [{ args: ['police', 300_000_000, 1_000_000,
26
+ 'exceed-action',
27
+ 'policed-dscp-transmit'],
28
+ commands: [{ args: %w(set dscp cs1),
29
+ commands: [], pos: 114 }],
30
+ pos: 50
31
+ }],
32
+ pos: 24
33
+ },
34
+
35
+ { args: %w(class other_service),
36
+ commands: [{ args: ['police', 600_000_000, 1_000_000,
37
+ 'exceed-action',
38
+ 'policed-dscp-transmit'],
39
+ commands: [{ args: %w(set dscp cs2),
40
+ commands: [], pos: 214 },
41
+ { args: ['command_with_no_args'],
42
+ commands: [], pos: 230 }],
43
+ pos: 150
44
+ }],
45
+ pos: 128
46
+ }],
47
+ pos: 0
48
+ }]
49
+ }
50
+ end
51
+
52
+ describe '#call' do
53
+ subject { klass.new.call(input) }
54
+ let(:subject_pure) do
55
+ klass.new(lexer: IOSParser::PureLexer.new).call(input)
56
+ end
57
+
58
+ it('constructs the right AST') { expect(subject.to_hash).to eq output }
59
+
60
+ it('constructs the right AST (using the pure-ruby lexer)') do
61
+ expect(subject_pure.to_hash[:commands]).to eq output[:commands]
62
+ end
63
+
64
+ it('can be searched by an exact command') do
65
+ expect(subject.find_all(name: 'set').map(&:to_hash))
66
+ .to eq [{ args: %w(set dscp cs1),
67
+ commands: [], pos: 114 },
68
+ { args: %w(set dscp cs2),
69
+ commands: [], pos: 214 }]
70
+ end
71
+
72
+ context 'can be searched by name and the first argument' do
73
+ let(:result) do
74
+ expect(subject.find_all(starts_with: starts_with).map(&:to_hash))
75
+ .to eq expectation
76
+ end
77
+
78
+ let(:expectation) { [output[:commands][0][:commands][1]] }
79
+
80
+ context 'with an array of strings' do
81
+ let(:starts_with) { %w(class other_service) }
82
+ it { result }
83
+ end
84
+
85
+ context 'with an array of regular expressions' do
86
+ let(:starts_with) { [/.lass/, /^other_[a-z]+$/] }
87
+ it { result }
88
+ end
89
+
90
+ context 'with a string, space-separated' do
91
+ let(:starts_with) { 'class other_service' }
92
+ it { result }
93
+ end
94
+
95
+ context 'integer argument' do
96
+ let(:expectation) do
97
+ [{ args: ['police', 300_000_000, 1_000_000, 'exceed-action',
98
+ 'policed-dscp-transmit'],
99
+ commands: [{ args: %w(set dscp cs1),
100
+ commands: [], pos: 114 }],
101
+ pos: 50
102
+ }]
103
+ end
104
+
105
+ context 'integer query' do
106
+ let(:starts_with) { ['police', 300_000_000] }
107
+ it { result }
108
+ end # context 'integer query'
109
+
110
+ context 'string query' do
111
+ let(:starts_with) { 'police 300000000' }
112
+ it { result }
113
+ end # context 'string query'
114
+ end
115
+ end # context 'integer argument'
116
+
117
+ context 'nested search' do
118
+ it 'queries can be chained' do
119
+ expect(subject
120
+ .find('policy-map').find('class').find('police')
121
+ .find('set')
122
+ .to_hash)
123
+ .to eq(args: %w(set dscp cs1),
124
+ commands: [], pos: 114)
125
+ end
126
+ end # context 'nested search'
127
+
128
+ context 'pass a block' do
129
+ it 'is evaluated for each matching command' do
130
+ ary = []
131
+ subject.find_all('class') { |cmd| ary << cmd.args[1] }
132
+ expect(ary).to eq %w(myservice_service other_service)
133
+ end
134
+ end # context 'pass a block'
135
+ end # end context 'indented region'
136
+
137
+ context '2950' do
138
+ let(:input) { <<END }
139
+ hostname myswitch1
140
+ vlan 3
141
+ name MyVlanName
142
+ interface FastEthernet0/1
143
+ speed 100
144
+ END
145
+
146
+ let(:output) { klass.new.call(input) }
147
+
148
+ it { expect(output.find('hostname').args[1]).to eq 'myswitch1' }
149
+
150
+ it('extracts vlan names') do
151
+ expect(output.find('vlan 3').find('name').args[1])
152
+ .to eq 'MyVlanName'
153
+ end
154
+
155
+ it('extracts interface speed') do
156
+ expect(output.find('interface FastEthernet0/1').find('speed').args[1])
157
+ .to eq 100
158
+ end
159
+
160
+ it('parses snmp commands') do
161
+ snmp_command = <<-END
162
+ snmp-server group my_group v3 auth read my_ro
163
+ END
164
+ result = klass.new.call(snmp_command)
165
+ expect(result[0].name).to eq 'snmp-server'
166
+ end
167
+
168
+ it('parses a simple alias') do
169
+ simple_alias = 'alias exec stat sh int | inc [1-9].*ignored|[1-9].*'\
170
+ "resets|Ethernet0|minute\n"
171
+ result = klass.new.call(simple_alias)
172
+ expect(result[0].name).to eq 'alias'
173
+ end
174
+
175
+ it('parses a complex alias') do
176
+ complex_alias = 'alias exec stats sh int | inc Ether.*'\
177
+ '(con|Loop.*is up|Vlan.*is up|Port-.*is up|'\
178
+ "input rate [^0]|output rate [^0]\n"
179
+ result = klass.new.call(complex_alias)
180
+ expect(result[0].name).to eq 'alias'
181
+ expect(result[0].args.last).to eq '[^0]'
182
+ end
183
+
184
+ it('parses a banner') do
185
+ banner_text = <<-END
186
+
187
+
188
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
189
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
190
+
191
+
192
+ END
193
+ banner_command = "banner exec ^C#{banner_text}^C\n"
194
+
195
+ result = klass.new.call(banner_command)
196
+ expect(result[0].args[2]).to eq banner_text
197
+ end
198
+
199
+ it('parses a crypto trustpoint section') do
200
+ text = <<END
201
+ crypto pki trustpoint TP-self-signed-0123456789
202
+ enrollment selfsigned
203
+ subject-name cn=IOS-Self-Signed-Certificate-1234567890
204
+ revocation-check none
205
+ rsakeypair TP-self-signed-2345678901
206
+ END
207
+ result = klass.new.call(text)
208
+ expect(result).not_to be_nil
209
+ end
210
+
211
+ it('parses a crypto certificate section') do
212
+ sp = ' '
213
+ text = <<END
214
+ crypto pki certificate chain TP-self-signed-1234567890
215
+ certificate self-signed 01
216
+ FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF#{sp}
217
+ EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE EEEEEEEE#{sp}
218
+ DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD#{sp}
219
+ CCCCCCCC CCCCCCCC
220
+ quit
221
+
222
+ END
223
+
224
+ result = klass.new.call(text)
225
+ expect(result).not_to be_nil
226
+ end
227
+
228
+ it('parses an MST configuration section') do
229
+ text = <<END
230
+ spanning-tree mst configuration
231
+ name MyMSTConfig
232
+ revision 1
233
+ instance 1 vlan 1-59, 4000
234
+ instance 2 vlan 90-99
235
+ instance 3 vlan 100-1500
236
+ instance 4 vlan 2000-3500, 4000
237
+ END
238
+
239
+ result = klass.new.call(text)
240
+ expect(result).not_to be_nil
241
+ end
242
+ end # context '2950'
243
+
244
+ it('finds various ip route formats') do
245
+ text = <<END
246
+ ip route 10.0.0.1 255.255.255.255 Null0
247
+ ip route 9.9.9.199 255.255.255.255 42.42.42.142 name PONIES
248
+ ip route vrf Mgmt-intf 0.0.0.0 0.0.0.0 9.9.9.199
249
+ ip route 0.0.0.0/0 11.11.0.111 120
250
+ END
251
+
252
+ result = klass.new.call(text)
253
+
254
+ cmd_ary = [
255
+ { args: ['ip', 'route', '10.0.0.1', '255.255.255.255',
256
+ 'Null0'],
257
+ commands: [], pos: 0 },
258
+ { args: ['ip', 'route', '9.9.9.199', '255.255.255.255',
259
+ '42.42.42.142', 'name', 'PONIES'],
260
+ commands: [], pos: 40 },
261
+ { args: ['ip', 'route', 'vrf', 'Mgmt-intf', '0.0.0.0',
262
+ '0.0.0.0', '9.9.9.199'],
263
+ commands: [], pos: 100 },
264
+ { args: ['ip', 'route', '0.0.0.0/0', '11.11.0.111', 120],
265
+ commands: [], pos: 149 }
266
+ ]
267
+
268
+ expect(result.find_all('ip route').map(&:to_hash)).to eq(cmd_ary)
269
+
270
+ expect(result.find_all('ip route 9.9.9.199').length).to eq 1
271
+
272
+ cmd_hash = { args: ['ip', 'route', '9.9.9.199', '255.255.255.255',
273
+ '42.42.42.142', 'name', 'PONIES'],
274
+ commands: [], pos: 40 }
275
+ expect(result.find('ip route 9.9.9.199').to_hash).to eq(cmd_hash)
276
+ end # end context '#call'
277
+
278
+ describe '#to_s' do
279
+ subject { klass.new.call(input) }
280
+ let(:police2) { <<END }
281
+ police 600000000 1000000 exceed-action policed-dscp-transmit
282
+ set dscp cs2
283
+ command_with_no_args
284
+ END
285
+
286
+ it('returns the string form of the original command(s)') do
287
+ expect(subject.to_s).to eq input
288
+ expect(subject.find('policy-map').to_s).to eq input
289
+ expect(subject.find('command_with_no_args').to_s)
290
+ .to eq " command_with_no_args\n"
291
+ expect(subject.find('police 600000000').to_s).to eq police2
292
+ end
293
+
294
+ context 'with dedent: true' do
295
+ it('returns the original without extra indentation') do
296
+ expect(subject.find('police 600000000').to_s(dedent: true))
297
+ .to eq police2.lines.map { |l| l[2..-1] }.join
298
+ end
299
+ end
300
+ end # describe '#to_s'
301
+
302
+ describe '#each' do
303
+ subject { klass.new.call(input) }
304
+ it 'traverses the AST' do
305
+ actual_paths = subject.map(&:path)
306
+ expected_paths = [
307
+ [],
308
+ ['policy-map mypolicy_in'],
309
+ ['policy-map mypolicy_in',
310
+ 'class myservice_service'],
311
+ ['policy-map mypolicy_in',
312
+ 'class myservice_service',
313
+ 'police 300000000 1000000 exceed-action policed-dscp-transmit'],
314
+ ['policy-map mypolicy_in'],
315
+ ['policy-map mypolicy_in',
316
+ 'class other_service'],
317
+ ['policy-map mypolicy_in',
318
+ 'class other_service',
319
+ 'police 600000000 1000000 exceed-action policed-dscp-transmit'],
320
+ ['policy-map mypolicy_in',
321
+ 'class other_service',
322
+ 'police 600000000 1000000 exceed-action policed-dscp-transmit']
323
+ ]
324
+ expect(actual_paths).to eq(expected_paths)
325
+ end
326
+ end # describe '#each'
327
+ end # context 'indented region'
328
+
329
+ context 'empty source' do
330
+ context 'when input is not a string' do
331
+ it 'raises ArgumentError' do
332
+ expect { klass.new.call [] }.to raise_error ArgumentError
333
+ expect { klass.new.call nil }.to raise_error ArgumentError
334
+ expect { klass.new.call 666 }.to raise_error ArgumentError
335
+ end
336
+ end # context 'when input is not a string' do
337
+ end # context 'empty source' do
338
+ end # end describe IOS
339
+ end # end module IOSParser