ios_parser 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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