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