rbeapi 0.5.1 → 1.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/CHANGELOG.md +211 -76
- data/Gemfile +14 -3
- data/README.md +74 -38
- data/Rakefile +38 -17
- data/gems/inifile/inifile.spec.tmpl +31 -4
- data/gems/net_http_unix/net_http_unix.spec.tmpl +34 -8
- data/gems/netaddr/netaddr.spec.tmpl +31 -5
- data/guide/getting-started.rst +95 -64
- data/guide/installation.rst +27 -6
- data/guide/release-notes.rst +5 -1
- data/guide/testing.rst +5 -2
- data/guide/upgrading.rst +2 -0
- data/lib/rbeapi/api/dns.rb +8 -2
- data/lib/rbeapi/api/interfaces.rb +107 -21
- data/lib/rbeapi/api/ipinterfaces.rb +48 -0
- data/lib/rbeapi/api/prefixlists.rb +53 -23
- data/lib/rbeapi/api/routemaps.rb +11 -0
- data/lib/rbeapi/api/stp.rb +6 -3
- data/lib/rbeapi/api/switchports.rb +5 -11
- data/lib/rbeapi/api/system.rb +1 -1
- data/lib/rbeapi/api/users.rb +2 -0
- data/lib/rbeapi/api/varp.rb +6 -0
- data/lib/rbeapi/api/vlans.rb +44 -0
- data/lib/rbeapi/api/vrrp.rb +13 -0
- data/lib/rbeapi/client.rb +19 -4
- data/lib/rbeapi/switchconfig.rb +330 -0
- data/lib/rbeapi/version.rb +1 -1
- data/rbeapi.gemspec +2 -0
- data/rbeapi.spec.tmpl +30 -3
- data/spec/fixtures/.gitignore +1 -0
- data/spec/support/matchers/switch_config_sections.rb +80 -0
- data/spec/system/rbeapi/api/interfaces_base_spec.rb +32 -3
- data/spec/system/rbeapi/api/interfaces_ethernet_spec.rb +56 -8
- data/spec/system/rbeapi/api/interfaces_portchannel_spec.rb +33 -1
- data/spec/system/rbeapi/api/interfaces_vxlan_spec.rb +27 -0
- data/spec/system/rbeapi/api/ipinterfaces_spec.rb +34 -1
- data/spec/system/rbeapi/api/prefixlists_spec.rb +198 -0
- data/spec/system/rbeapi/api/stp_instances_spec.rb +49 -5
- data/spec/system/rbeapi/api/switchports_spec.rb +15 -9
- data/spec/system/rbeapi/api/vlans_spec.rb +46 -0
- data/spec/unit/rbeapi/api/interfaces/base_spec.rb +1 -1
- data/spec/unit/rbeapi/api/interfaces/ethernet_spec.rb +1 -1
- data/spec/unit/rbeapi/api/interfaces/portchannel_spec.rb +9 -2
- data/spec/unit/rbeapi/api/interfaces/vxlan_spec.rb +1 -1
- data/spec/unit/rbeapi/api/prefixlists/default_spec.rb +202 -0
- data/spec/unit/rbeapi/api/prefixlists/fixture_prefixlists.text +11 -0
- data/spec/unit/rbeapi/api/routemaps/default_spec.rb +5 -0
- data/spec/unit/rbeapi/api/switchports/default_spec.rb +4 -4
- data/spec/unit/rbeapi/api/system/default_spec.rb +5 -0
- data/spec/unit/rbeapi/api/system/fixture_system.text +1 -0
- data/spec/unit/rbeapi/api/vlans/default_spec.rb +30 -0
- data/spec/unit/rbeapi/api/vrrp/default_spec.rb +10 -0
- data/spec/unit/rbeapi/client_spec.rb +42 -0
- data/spec/unit/rbeapi/switchconfig2_spec.rb +119 -0
- data/spec/unit/rbeapi/switchconfig3_spec.rb +125 -0
- data/spec/unit/rbeapi/switchconfig_spec.rb +335 -0
- metadata +21 -7
@@ -43,22 +43,29 @@ module Rbeapi
|
|
43
43
|
#
|
44
44
|
class Prefixlists < Entity
|
45
45
|
##
|
46
|
-
# Returns the
|
46
|
+
# Returns the prefix list configured on the node.
|
47
47
|
#
|
48
48
|
# @example
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
49
|
+
# [
|
50
|
+
# {
|
51
|
+
# seq: <string>,
|
52
|
+
# action: <string>,
|
53
|
+
# prefix: <string>
|
54
|
+
# },
|
55
|
+
# ...,
|
56
|
+
# {
|
57
|
+
# seq: <string>,
|
58
|
+
# action: <string>,
|
59
|
+
# prefix: <string>
|
53
60
|
# }
|
54
|
-
#
|
61
|
+
# ]
|
55
62
|
#
|
56
63
|
# @param name [String] The name of the prefix-list to return.
|
57
64
|
#
|
58
|
-
# @return [Hash
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
65
|
+
# @return [Array<Hash>] The method will return the configured
|
66
|
+
# prefix list on the node with all its sequences as a Ruby
|
67
|
+
# array of hashes, where each prefix is a hash object.
|
68
|
+
# If the prefix list is not found, a nil object is returned.
|
62
69
|
def get(name)
|
63
70
|
config = get_block("ip prefix-list #{name}")
|
64
71
|
return nil unless config
|
@@ -66,25 +73,48 @@ module Rbeapi
|
|
66
73
|
entries = config.scan(/^\s{3}(?:seq\s)(\d+)\s(permit|deny)\s(.+)$/)
|
67
74
|
entries.each_with_object([]) do |entry, arry|
|
68
75
|
arry << { 'seq' => entry[0], 'action' => entry[1],
|
69
|
-
'
|
76
|
+
'prefix' => entry[2] }
|
70
77
|
end
|
71
78
|
end
|
72
79
|
|
73
80
|
##
|
74
|
-
# Returns
|
81
|
+
# Returns all prefix lists configured on the node.
|
75
82
|
#
|
76
83
|
# @example
|
77
84
|
# {
|
78
|
-
# <
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
85
|
+
# <name1>: [
|
86
|
+
# {
|
87
|
+
# seq: <string>,
|
88
|
+
# action: <string>,
|
89
|
+
# prefix: <string>
|
90
|
+
# },
|
91
|
+
# ...
|
92
|
+
# {
|
93
|
+
# seq: <string>,
|
94
|
+
# action: <string>,
|
95
|
+
# prefix: <string>
|
96
|
+
# }
|
97
|
+
# ],
|
98
|
+
# ...,
|
99
|
+
# <nameN>: [
|
100
|
+
# {
|
101
|
+
# seq: <string>,
|
102
|
+
# action: <string>,
|
103
|
+
# prefix: <string>
|
104
|
+
# },
|
105
|
+
# ...
|
106
|
+
# {
|
107
|
+
# seq: <string>,
|
108
|
+
# action: <string>,
|
109
|
+
# prefix: <string>
|
110
|
+
# }
|
111
|
+
# ]
|
82
112
|
# }
|
83
113
|
#
|
84
|
-
# @return [Hash<String,
|
85
|
-
#
|
86
|
-
# there are no
|
87
|
-
#
|
114
|
+
# @return [Hash<String, Array>] The method will return all the
|
115
|
+
# prefix lists configured on the node as a Ruby hash object.
|
116
|
+
# If there are no prefix lists configured, an empty hash will
|
117
|
+
# be returned.
|
88
118
|
def getall
|
89
119
|
lists = config.scan(/(?<=^ip\sprefix-list\s).+/)
|
90
120
|
lists.each_with_object({}) do |name, hsh|
|
@@ -100,7 +130,7 @@ module Rbeapi
|
|
100
130
|
#
|
101
131
|
# @return [Boolean] Returns true if the command completed successfully.
|
102
132
|
def create(name)
|
103
|
-
configure
|
133
|
+
configure("ip prefix-list #{name}")
|
104
134
|
end
|
105
135
|
|
106
136
|
##
|
@@ -120,7 +150,7 @@ module Rbeapi
|
|
120
150
|
cmd = "ip prefix-list #{name}"
|
121
151
|
cmd << " seq #{seq}" if seq
|
122
152
|
cmd << " #{action} #{prefix}"
|
123
|
-
configure
|
153
|
+
configure(cmd)
|
124
154
|
end
|
125
155
|
|
126
156
|
##
|
@@ -134,7 +164,7 @@ module Rbeapi
|
|
134
164
|
def delete(name, seq = nil)
|
135
165
|
cmd = "no ip prefix-list #{name}"
|
136
166
|
cmd << " seq #{seq}" if seq
|
137
|
-
configure
|
167
|
+
configure(cmd)
|
138
168
|
end
|
139
169
|
end
|
140
170
|
end
|
data/lib/rbeapi/api/routemaps.rb
CHANGED
@@ -304,6 +304,9 @@ module Rbeapi
|
|
304
304
|
if opts.empty?
|
305
305
|
cmds = name_commands(name, action, seqno)
|
306
306
|
else
|
307
|
+
if opts[:match] && !opts[:match].is_a?(Array)
|
308
|
+
fail ArgumentError, 'opts match must be an Array'
|
309
|
+
end
|
307
310
|
cmds = name_commands(name, action, seqno, opts)
|
308
311
|
if opts[:description]
|
309
312
|
cmds << 'no description'
|
@@ -343,6 +346,8 @@ module Rbeapi
|
|
343
346
|
#
|
344
347
|
# @return [Boolean] Returns true if the command completed successfully.
|
345
348
|
def remove_match_statements(name, action, seqno, cmds)
|
349
|
+
fail ArgumentError, 'cmds must be an Array' unless cmds.is_a?(Array)
|
350
|
+
|
346
351
|
entries = parse_entries(name)
|
347
352
|
return nil unless entries
|
348
353
|
entries.each do |entry|
|
@@ -369,6 +374,8 @@ module Rbeapi
|
|
369
374
|
#
|
370
375
|
# @return [Boolean] Returns true if the command completed successfully.
|
371
376
|
def remove_set_statements(name, action, seqno, cmds)
|
377
|
+
fail ArgumentError, 'cmds must be an Array' unless cmds.is_a?(Array)
|
378
|
+
|
372
379
|
entries = parse_entries(name)
|
373
380
|
return nil unless entries
|
374
381
|
entries.each do |entry|
|
@@ -439,6 +446,8 @@ module Rbeapi
|
|
439
446
|
#
|
440
447
|
# @return [Boolean] Returns true if the command completed successfully.
|
441
448
|
def set_match_statements(name, action, seqno, value)
|
449
|
+
fail ArgumentError, 'value must be an Array' unless value.is_a?(Array)
|
450
|
+
|
442
451
|
cmds = ["route-map #{name} #{action} #{seqno}"]
|
443
452
|
remove_match_statements(name, action, seqno, cmds)
|
444
453
|
Array(value).each do |options|
|
@@ -464,6 +473,8 @@ module Rbeapi
|
|
464
473
|
#
|
465
474
|
# @return [Boolean] Returns true if the command completed successfully.
|
466
475
|
def set_set_statements(name, action, seqno, value)
|
476
|
+
fail ArgumentError, 'value must be an Array' unless value.is_a?(Array)
|
477
|
+
|
467
478
|
cmds = ["route-map #{name} #{action} #{seqno}"]
|
468
479
|
remove_set_statements(name, action, seqno, cmds)
|
469
480
|
Array(value).each do |options|
|
data/lib/rbeapi/api/stp.rb
CHANGED
@@ -196,15 +196,18 @@ module Rbeapi
|
|
196
196
|
|
197
197
|
##
|
198
198
|
# parse_instances will scan the nodes current configuration and extract
|
199
|
-
# the list of configured mst instances.
|
200
|
-
#
|
199
|
+
# the list of configured mst instances. Instances 0 and 1 are defined by
|
200
|
+
# default in the switch config and are always returned, even if not
|
201
|
+
# visible in the 'spanning-tree mst configuration' config section.
|
201
202
|
#
|
202
203
|
# @api private
|
203
204
|
#
|
204
205
|
# @return [Array<String>] Returns an Array of configured stp instances.
|
205
206
|
def parse_instances
|
206
207
|
config = get_block('spanning-tree mst configuration')
|
207
|
-
config.scan(/(?<=^\s{3}instance\s)\d+/)
|
208
|
+
response = config.scan(/(?<=^\s{3}instance\s)\d+/)
|
209
|
+
response.push('0', '1').uniq!
|
210
|
+
response
|
208
211
|
end
|
209
212
|
private :parse_instances
|
210
213
|
|
@@ -136,12 +136,7 @@ module Rbeapi
|
|
136
136
|
return { trunk_allowed_vlans: [] } unless mdata[1] != 'none'
|
137
137
|
vlans = mdata[1].split(',')
|
138
138
|
values = vlans.each_with_object([]) do |vlan, arry|
|
139
|
-
|
140
|
-
arry << vlan.to_i
|
141
|
-
else
|
142
|
-
range_start, range_end = vlan.split('-')
|
143
|
-
arry.push(*Array(range_start.to_i..range_end.to_i))
|
144
|
-
end
|
139
|
+
arry << vlan.to_s
|
145
140
|
end
|
146
141
|
{ trunk_allowed_vlans: values }
|
147
142
|
end
|
@@ -264,9 +259,9 @@ module Rbeapi
|
|
264
259
|
#
|
265
260
|
# @param opts [Hash] The configuration parameters for the interface.
|
266
261
|
#
|
267
|
-
# @option
|
262
|
+
# @option opts value [Array] The list of vlan ids to configure on the
|
268
263
|
# switchport to be allowed. This value must be an array of valid vlan
|
269
|
-
# ids.
|
264
|
+
# ids or vlan ranges.
|
270
265
|
#
|
271
266
|
# @option opts enable [Boolean] If false then the command is
|
272
267
|
# negated. Default is true.
|
@@ -283,7 +278,7 @@ module Rbeapi
|
|
283
278
|
|
284
279
|
if value
|
285
280
|
fail ArgumentError, 'value must be an Array' unless value.is_a?(Array)
|
286
|
-
value = value.map(&:inspect).join(',')
|
281
|
+
value = value.map(&:inspect).join(',').tr('"', '')
|
287
282
|
end
|
288
283
|
|
289
284
|
case default
|
@@ -293,8 +288,7 @@ module Rbeapi
|
|
293
288
|
if !enable
|
294
289
|
cmds = 'no switchport trunk allowed vlan'
|
295
290
|
else
|
296
|
-
cmds = [
|
297
|
-
"switchport trunk allowed vlan #{value}"]
|
291
|
+
cmds = ["switchport trunk allowed vlan #{value}"]
|
298
292
|
end
|
299
293
|
end
|
300
294
|
configure_interface(name, cmds)
|
data/lib/rbeapi/api/system.rb
CHANGED
@@ -92,7 +92,7 @@ module Rbeapi
|
|
92
92
|
#
|
93
93
|
# @return [Hash<Symbol, Object>] The resource hash attribute.
|
94
94
|
def parse_iprouting(config)
|
95
|
-
mdata = /no\sip\srouting
|
95
|
+
mdata = /no\sip\srouting$/.match(config)
|
96
96
|
{ iprouting: mdata.nil? ? true : false }
|
97
97
|
end
|
98
98
|
private :parse_iprouting
|
data/lib/rbeapi/api/users.rb
CHANGED
@@ -149,6 +149,8 @@ module Rbeapi
|
|
149
149
|
#
|
150
150
|
# @return [Hash<Symbol, Object>] Returns the resource hash attribute.
|
151
151
|
def parse_user_entry(user)
|
152
|
+
fail ArgumentError, 'user must be an Array' unless user.is_a?(Array)
|
153
|
+
|
152
154
|
hsh = {}
|
153
155
|
hsh[:name] = user[0]
|
154
156
|
hsh[:privilege] = user[1].to_i
|
data/lib/rbeapi/api/varp.rb
CHANGED
@@ -199,12 +199,17 @@ module Rbeapi
|
|
199
199
|
# @option opts default [Boolean] The value should be set to default.
|
200
200
|
#
|
201
201
|
# @return [Boolean] True if the commands succeeds otherwise False.
|
202
|
+
# rubocop:disable Metrics/MethodLength
|
202
203
|
def set_addresses(name, opts = {})
|
203
204
|
value = opts[:value]
|
204
205
|
enable = opts.fetch(:enable, true)
|
205
206
|
default = opts[:default] || false
|
206
207
|
cmds = ["interface #{name}"]
|
207
208
|
|
209
|
+
if value
|
210
|
+
fail ArgumentError, 'value must be an Array' unless value.is_a?(Array)
|
211
|
+
end
|
212
|
+
|
208
213
|
case default
|
209
214
|
when true
|
210
215
|
cmds << 'default ip virtual-router address'
|
@@ -220,6 +225,7 @@ module Rbeapi
|
|
220
225
|
end
|
221
226
|
configure(cmds)
|
222
227
|
end
|
228
|
+
# rubocop:enable Metrics/MethodLength
|
223
229
|
|
224
230
|
##
|
225
231
|
# The add_address method assigns one virtual IPv4 address.
|
data/lib/rbeapi/api/vlans.rb
CHANGED
@@ -333,6 +333,50 @@ module Rbeapi
|
|
333
333
|
def remove_trunk_group(id, value)
|
334
334
|
configure(["vlan #{id}", "no trunk group #{value}"])
|
335
335
|
end
|
336
|
+
|
337
|
+
##
|
338
|
+
# Configures the trunk groups for the specified vlan.
|
339
|
+
# Trunk groups not currently set are added and trunk groups
|
340
|
+
# currently configured but not in the passed in value array are removed.
|
341
|
+
#
|
342
|
+
# @param name [String] The name of the vlan to configure.
|
343
|
+
#
|
344
|
+
# @param opts [Hash] The configuration parameters for the vlan.
|
345
|
+
#
|
346
|
+
# @option opts value [string] Set of values to configure the trunk group.
|
347
|
+
#
|
348
|
+
# @option opts enable [Boolean] If false then the command is
|
349
|
+
# negated. Default is true.
|
350
|
+
#
|
351
|
+
# @option opts default [Boolean] The value should be set to default
|
352
|
+
# Default takes precedence over enable.
|
353
|
+
#
|
354
|
+
# @return [Boolean] Returns True if the commands succeed otherwise False.
|
355
|
+
def set_trunk_groups(name, opts = {})
|
356
|
+
default = opts.fetch(:default, false)
|
357
|
+
return configure(["vlan #{name}", 'default trunk group']) if default
|
358
|
+
|
359
|
+
enable = opts.fetch(:enable, true)
|
360
|
+
return configure(["vlan #{name}", 'no trunk group']) unless enable
|
361
|
+
|
362
|
+
value = opts.fetch(:value, [])
|
363
|
+
fail ArgumentError, 'value must be an Array' unless value.is_a?(Array)
|
364
|
+
|
365
|
+
value = Set.new value
|
366
|
+
current_value = Set.new get(name)[:trunk_groups]
|
367
|
+
|
368
|
+
cmds = ["vlan #{name}"]
|
369
|
+
# Add trunk groups that are not currently in the list.
|
370
|
+
value.difference(current_value).each do |group|
|
371
|
+
cmds << "trunk group #{group}"
|
372
|
+
end
|
373
|
+
|
374
|
+
# Remove trunk groups that are not in the new list.
|
375
|
+
current_value.difference(value).each do |group|
|
376
|
+
cmds << "no trunk group #{group}"
|
377
|
+
end
|
378
|
+
configure(cmds) if cmds.length > 1
|
379
|
+
end
|
336
380
|
end
|
337
381
|
end
|
338
382
|
end
|
data/lib/rbeapi/api/vrrp.rb
CHANGED
@@ -510,8 +510,19 @@ module Rbeapi
|
|
510
510
|
# of the tracked interface, and the amount to decrement the priority.
|
511
511
|
#
|
512
512
|
# @return [Boolean] Returns true if the command completed successfully.
|
513
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize,
|
514
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
513
515
|
def create(name, vrid, opts = {})
|
514
516
|
fail ArgumentError, 'create has no options set' if opts.empty?
|
517
|
+
|
518
|
+
if opts[:secondary_ip] && !opts[:secondary_ip].is_a?(Array)
|
519
|
+
fail ArgumentError, 'opts secondary_ip must be an Array'
|
520
|
+
end
|
521
|
+
|
522
|
+
if opts[:track] && !opts[:track].is_a?(Array)
|
523
|
+
fail ArgumentError, 'opts track must be an Array'
|
524
|
+
end
|
525
|
+
|
515
526
|
cmds = []
|
516
527
|
if opts.key?(:enable)
|
517
528
|
if opts[:enable]
|
@@ -561,6 +572,8 @@ module Rbeapi
|
|
561
572
|
cmds += build_tracks_cmd(name, vrid, opts[:track]) if opts.key?(:track)
|
562
573
|
configure_interface(name, cmds)
|
563
574
|
end
|
575
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize,
|
576
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
564
577
|
|
565
578
|
##
|
566
579
|
# delete will delete the virtual router ID on the interface from the
|
data/lib/rbeapi/client.rb
CHANGED
@@ -506,14 +506,29 @@ module Rbeapi
|
|
506
506
|
# interfaces Filter config to include only the given interfaces
|
507
507
|
# section Display sections containing matching commands
|
508
508
|
#
|
509
|
-
# @return [String] The specified configuration as text
|
509
|
+
# @return [String] The specified configuration as text or nil if no
|
510
|
+
# config is found. When encoding is set to json, returns
|
511
|
+
# a hash.
|
510
512
|
def get_config(opts = {})
|
511
513
|
config = opts.fetch(:config, 'running-config')
|
512
514
|
params = opts.fetch(:params, '')
|
515
|
+
encoding = opts.fetch(:encoding, 'text')
|
513
516
|
as_string = opts.fetch(:as_string, false)
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
+
begin
|
518
|
+
result = run_commands("show #{config} #{params}", encoding: encoding)
|
519
|
+
rescue Rbeapi::Eapilib::CommandError => error
|
520
|
+
if ( error.to_s =~ /'show (running|startup)-config'/ )
|
521
|
+
return nil
|
522
|
+
else
|
523
|
+
raise error
|
524
|
+
end
|
525
|
+
end
|
526
|
+
if encoding == 'json'
|
527
|
+
return result.first
|
528
|
+
else
|
529
|
+
return result.first['output'].strip.split("\n") unless as_string
|
530
|
+
result.first['output'].strip
|
531
|
+
end
|
517
532
|
end
|
518
533
|
|
519
534
|
##
|
@@ -0,0 +1,330 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2016, Arista Networks, Inc.
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are
|
7
|
+
# met:
|
8
|
+
#
|
9
|
+
# Redistributions of source code must retain the above copyright notice,
|
10
|
+
# this list of conditions and the following disclaimer.
|
11
|
+
#
|
12
|
+
# Redistributions in binary form must reproduce the above copyright
|
13
|
+
# notice, this list of conditions and the following disclaimer in the
|
14
|
+
# documentation and/or other materials provided with the distribution.
|
15
|
+
#
|
16
|
+
# Neither the name of Arista Networks nor the names of its
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
18
|
+
# this software without specific prior written permission.
|
19
|
+
#
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
21
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
22
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
23
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS
|
24
|
+
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
25
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
26
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
27
|
+
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
28
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
29
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
30
|
+
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
31
|
+
#
|
32
|
+
|
33
|
+
##
|
34
|
+
# Rbeapi toplevel namespace
|
35
|
+
module Rbeapi
|
36
|
+
##
|
37
|
+
# Rbeapi::SwitchConfig
|
38
|
+
module SwitchConfig
|
39
|
+
##
|
40
|
+
# Section class
|
41
|
+
#
|
42
|
+
# A switch configuration section consists of the command line that
|
43
|
+
# enters into the configuration mode, an array of command strings
|
44
|
+
# that are executed in the current configuration mode, a reference
|
45
|
+
# to the parent section, and an array of refereces to all sub-sections
|
46
|
+
# contained within this section. A sub-section is a nested configuration
|
47
|
+
# mode.
|
48
|
+
#
|
49
|
+
# Read Accessors for following class instance variables:
|
50
|
+
# line: <string>,
|
51
|
+
# parent: <Section>,
|
52
|
+
# cmds: array<strings>,
|
53
|
+
# children: array<Section>
|
54
|
+
#
|
55
|
+
class Section
|
56
|
+
attr_reader :line
|
57
|
+
attr_reader :parent
|
58
|
+
attr_reader :cmds
|
59
|
+
attr_reader :children
|
60
|
+
|
61
|
+
##
|
62
|
+
# The Section class contains a parsed section of switch config.
|
63
|
+
#
|
64
|
+
# @param config [String] A string containing the switch configuration.
|
65
|
+
#
|
66
|
+
# @return [Section] Returns an instance of Section
|
67
|
+
|
68
|
+
def initialize(line, parent)
|
69
|
+
@line = line
|
70
|
+
@parent = parent
|
71
|
+
@cmds = []
|
72
|
+
@children = []
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Add a child to the end of the children array.
|
77
|
+
#
|
78
|
+
# @param child [Section] A Section class instance.
|
79
|
+
def add_child(child)
|
80
|
+
@children.push(child)
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Add a cmd to the end of the cmds array if it is not already in
|
85
|
+
# the cmd array.
|
86
|
+
#
|
87
|
+
# @param cmd [String] A command string that is added to the cmds array.
|
88
|
+
def add_cmd(cmd)
|
89
|
+
@cmds.push(cmd) unless @cmds.include?(cmd)
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Return the child that has the specified line (command mode).
|
94
|
+
#
|
95
|
+
# @param line [String] The mode command for this section.
|
96
|
+
def get_child(line)
|
97
|
+
@children.each do |child|
|
98
|
+
return child if child.line == line
|
99
|
+
end
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Private campare method to compare the commands between two Section
|
105
|
+
# classes.
|
106
|
+
#
|
107
|
+
# @param cmds2 [Array<String>] An array of commands.
|
108
|
+
#
|
109
|
+
# @return [Array<String>] The array of commands in @cmds that are not
|
110
|
+
# in cmds2. The array is empty if @cmds equals cmds2.
|
111
|
+
def _compare_cmds(cmds2)
|
112
|
+
c1 = Set.new(@cmds)
|
113
|
+
c2 = Set.new(cmds2)
|
114
|
+
# Compare the commands and return the difference as an array of strings
|
115
|
+
c1.difference(c2).to_a
|
116
|
+
end
|
117
|
+
private :_compare_cmds
|
118
|
+
|
119
|
+
##
|
120
|
+
# Campare method to compare two Section classes.
|
121
|
+
# The comparison will recurse through all the children in the Sections.
|
122
|
+
# The parent is ignored at the top level section. Only call this
|
123
|
+
# method if self and section2 have the same line.
|
124
|
+
#
|
125
|
+
# @param section2 [Section] An instance of a Section class to compare.
|
126
|
+
#
|
127
|
+
# @return [Section] The Section object contains the portion of self
|
128
|
+
# that is not in section2.
|
129
|
+
def compare_r(section2)
|
130
|
+
fail '@line must equal section2.line' if @line != section2.line
|
131
|
+
|
132
|
+
# XXX Need to have a list of exceptions of mode commands that
|
133
|
+
# support default. If all the commands have been removed from
|
134
|
+
# that section in the new config then the old config just wants
|
135
|
+
# to default the mode command.
|
136
|
+
# ex: spanning-tree mst configuration
|
137
|
+
# instance 1 vlan 1
|
138
|
+
# Currently generates this error:
|
139
|
+
# ' default instance 1 vlan 1' failed: invalid command
|
140
|
+
|
141
|
+
results = Section.new(@line, nil)
|
142
|
+
|
143
|
+
# Compare the commands
|
144
|
+
diff_cmds = _compare_cmds(section2.cmds)
|
145
|
+
diff_cmds.each do |cmd|
|
146
|
+
results.add_cmd(cmd)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Using a depth first search to recursively descend through the
|
150
|
+
# children doing a comparison.
|
151
|
+
@children.each do |s1_child|
|
152
|
+
s2_child = section2.get_child(s1_child.line)
|
153
|
+
if s2_child
|
154
|
+
# Sections Match based on the line. Compare the children
|
155
|
+
# and if there are differences add them to the results.
|
156
|
+
res = s1_child.compare_r(s2_child)
|
157
|
+
if !res.children.empty? || !res.cmds.empty?
|
158
|
+
results.add_child(res)
|
159
|
+
results.add_cmd(s1_child.line)
|
160
|
+
end
|
161
|
+
else
|
162
|
+
# Section 1 has child, but section 2 does not, add to results
|
163
|
+
results.add_child(s1_child.clone)
|
164
|
+
results.add_cmd(s1_child.line)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
results
|
169
|
+
end
|
170
|
+
|
171
|
+
##
|
172
|
+
# Campare a Section class to the current section.
|
173
|
+
# The comparison will recurse through all the children in the Sections.
|
174
|
+
# The parent is ignored at the top level section.
|
175
|
+
#
|
176
|
+
# @param section2 [Section] An instance of a Section class to compare.
|
177
|
+
#
|
178
|
+
# @return [Array<Section>] Returns an array of 2 Section objects. The
|
179
|
+
# first Section object contains the portion of self that is not
|
180
|
+
# in section2. The second Section object returned is the portion of
|
181
|
+
# section2 that is not in self.
|
182
|
+
def compare(section2)
|
183
|
+
if @line != section2.line
|
184
|
+
fail 'XXX What if @line does not equal section2.line'
|
185
|
+
end
|
186
|
+
|
187
|
+
results = []
|
188
|
+
# Compare self with section2
|
189
|
+
results[0] = compare_r(section2)
|
190
|
+
# Compare section2 with self
|
191
|
+
results[1] = section2.compare_r(self)
|
192
|
+
results
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# SwitchConfig class
|
198
|
+
class SwitchConfig
|
199
|
+
attr_accessor :name
|
200
|
+
attr_reader :global
|
201
|
+
|
202
|
+
##
|
203
|
+
# The SwitchConfig class will parse a string containing a switch
|
204
|
+
# configuration and return an instance of a SwitchConfig. The
|
205
|
+
# SwitchConfig contains the global section which contains
|
206
|
+
# references to all sub-sections (children).
|
207
|
+
#
|
208
|
+
# {
|
209
|
+
# global: <Section>,
|
210
|
+
# }
|
211
|
+
#
|
212
|
+
# @param config [String] A string containing the switch configuration.
|
213
|
+
#
|
214
|
+
# @return [Section] Returns an instance of Section
|
215
|
+
def initialize(config)
|
216
|
+
@indent = 3
|
217
|
+
@multiline_cmds = ['^banner', '^\s*ssl key', '^\s*ssl certificate',
|
218
|
+
'^\s*protocol https certificate']
|
219
|
+
chk_format(config)
|
220
|
+
parse(config)
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Check format on a switch configuration string.
|
225
|
+
#
|
226
|
+
# Verify that the indentation is correct on the switch configuration.
|
227
|
+
#
|
228
|
+
# @param config [String] A string containing the switch configuration.
|
229
|
+
#
|
230
|
+
# @return [boolean] Returns true if format is good, otherwise raises
|
231
|
+
# an argument error.
|
232
|
+
def chk_format(config)
|
233
|
+
skip = false
|
234
|
+
config.each_line do |line|
|
235
|
+
skip = true if @multiline_cmds.any? { |cmd| line =~ /#{cmd}/ }
|
236
|
+
if skip
|
237
|
+
if line =~ /^\s*EOF$/
|
238
|
+
skip = false
|
239
|
+
else
|
240
|
+
next
|
241
|
+
end
|
242
|
+
end
|
243
|
+
ind = line[/\A */].size
|
244
|
+
if ind % @indent != 0
|
245
|
+
fail ArgumentError, 'SwitchConfig indentation must be multiple of '\
|
246
|
+
"#{@indent} improper indent #{ind}: #{line}"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
true
|
250
|
+
end
|
251
|
+
private :chk_format
|
252
|
+
|
253
|
+
##
|
254
|
+
# Parse a switch configuration into sections.
|
255
|
+
#
|
256
|
+
# Parse a switch configuration and return a Config class.
|
257
|
+
# A switch configuration consists of the global section that contains
|
258
|
+
# a reference to all switch configuration sub-sections (children).
|
259
|
+
# Lines starting with '!' (comments) are ignored
|
260
|
+
#
|
261
|
+
# @param config [String] A string containing the switch configuration.
|
262
|
+
# rubocop:disable Metrics/MethodLength
|
263
|
+
def parse(config)
|
264
|
+
# Setup global section
|
265
|
+
section = Section.new('', nil)
|
266
|
+
@global = section
|
267
|
+
|
268
|
+
prev_indent = 0
|
269
|
+
prev_line = ''
|
270
|
+
combine = false
|
271
|
+
longline = []
|
272
|
+
|
273
|
+
config.each_line do |line|
|
274
|
+
if @multiline_cmds.any? { |cmd| line =~ /#{cmd}/ }
|
275
|
+
longline = []
|
276
|
+
combine = true
|
277
|
+
end
|
278
|
+
if combine
|
279
|
+
longline << line
|
280
|
+
if line =~ /^\s*EOF$/
|
281
|
+
line = longline.join
|
282
|
+
combine = false
|
283
|
+
else
|
284
|
+
next
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Ignore comment lines and the end statement if there
|
289
|
+
# XXX Fix parsing end
|
290
|
+
next if line.start_with?('!') || line.start_with?('end')
|
291
|
+
line.chomp!
|
292
|
+
next if line.empty?
|
293
|
+
indent_level = line[/\A */].size / @indent
|
294
|
+
if indent_level > prev_indent
|
295
|
+
# New section
|
296
|
+
section = Section.new(prev_line, section)
|
297
|
+
section.parent.add_child(section)
|
298
|
+
prev_indent = indent_level
|
299
|
+
elsif indent_level < prev_indent
|
300
|
+
# XXX This has a bug if we pop more than one section
|
301
|
+
# XXX Bug if we have 2 subsections with intervening commands
|
302
|
+
# End of current section
|
303
|
+
section = section.parent
|
304
|
+
prev_indent = indent_level
|
305
|
+
end
|
306
|
+
# Add the line to the current section
|
307
|
+
section.add_cmd(line)
|
308
|
+
prev_line = line
|
309
|
+
end
|
310
|
+
end
|
311
|
+
private :parse
|
312
|
+
# rubocop:enable Metrics/MethodLength
|
313
|
+
|
314
|
+
##
|
315
|
+
# Campare the current SwitchConfig class with another SwitchConfig class.
|
316
|
+
#
|
317
|
+
# @param switch_config [SwitchConfig] An instance of a SwitchConfig
|
318
|
+
# class to compare with the current instance.
|
319
|
+
#
|
320
|
+
# @return [Array<Sections>] Returns an array of 2 Section objects. The
|
321
|
+
# first Section object contains the portion of the current
|
322
|
+
# SwitchConfig instance that is not in the passed in switch_config. The
|
323
|
+
# second Section object is the portion of the passed in switch_config
|
324
|
+
# that is not in the current SwitchConfig instance.
|
325
|
+
def compare(switch_config)
|
326
|
+
@global.compare(switch_config.global)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|