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.
Files changed (57) hide show
  1. data/CHANGELOG.md +211 -76
  2. data/Gemfile +14 -3
  3. data/README.md +74 -38
  4. data/Rakefile +38 -17
  5. data/gems/inifile/inifile.spec.tmpl +31 -4
  6. data/gems/net_http_unix/net_http_unix.spec.tmpl +34 -8
  7. data/gems/netaddr/netaddr.spec.tmpl +31 -5
  8. data/guide/getting-started.rst +95 -64
  9. data/guide/installation.rst +27 -6
  10. data/guide/release-notes.rst +5 -1
  11. data/guide/testing.rst +5 -2
  12. data/guide/upgrading.rst +2 -0
  13. data/lib/rbeapi/api/dns.rb +8 -2
  14. data/lib/rbeapi/api/interfaces.rb +107 -21
  15. data/lib/rbeapi/api/ipinterfaces.rb +48 -0
  16. data/lib/rbeapi/api/prefixlists.rb +53 -23
  17. data/lib/rbeapi/api/routemaps.rb +11 -0
  18. data/lib/rbeapi/api/stp.rb +6 -3
  19. data/lib/rbeapi/api/switchports.rb +5 -11
  20. data/lib/rbeapi/api/system.rb +1 -1
  21. data/lib/rbeapi/api/users.rb +2 -0
  22. data/lib/rbeapi/api/varp.rb +6 -0
  23. data/lib/rbeapi/api/vlans.rb +44 -0
  24. data/lib/rbeapi/api/vrrp.rb +13 -0
  25. data/lib/rbeapi/client.rb +19 -4
  26. data/lib/rbeapi/switchconfig.rb +330 -0
  27. data/lib/rbeapi/version.rb +1 -1
  28. data/rbeapi.gemspec +2 -0
  29. data/rbeapi.spec.tmpl +30 -3
  30. data/spec/fixtures/.gitignore +1 -0
  31. data/spec/support/matchers/switch_config_sections.rb +80 -0
  32. data/spec/system/rbeapi/api/interfaces_base_spec.rb +32 -3
  33. data/spec/system/rbeapi/api/interfaces_ethernet_spec.rb +56 -8
  34. data/spec/system/rbeapi/api/interfaces_portchannel_spec.rb +33 -1
  35. data/spec/system/rbeapi/api/interfaces_vxlan_spec.rb +27 -0
  36. data/spec/system/rbeapi/api/ipinterfaces_spec.rb +34 -1
  37. data/spec/system/rbeapi/api/prefixlists_spec.rb +198 -0
  38. data/spec/system/rbeapi/api/stp_instances_spec.rb +49 -5
  39. data/spec/system/rbeapi/api/switchports_spec.rb +15 -9
  40. data/spec/system/rbeapi/api/vlans_spec.rb +46 -0
  41. data/spec/unit/rbeapi/api/interfaces/base_spec.rb +1 -1
  42. data/spec/unit/rbeapi/api/interfaces/ethernet_spec.rb +1 -1
  43. data/spec/unit/rbeapi/api/interfaces/portchannel_spec.rb +9 -2
  44. data/spec/unit/rbeapi/api/interfaces/vxlan_spec.rb +1 -1
  45. data/spec/unit/rbeapi/api/prefixlists/default_spec.rb +202 -0
  46. data/spec/unit/rbeapi/api/prefixlists/fixture_prefixlists.text +11 -0
  47. data/spec/unit/rbeapi/api/routemaps/default_spec.rb +5 -0
  48. data/spec/unit/rbeapi/api/switchports/default_spec.rb +4 -4
  49. data/spec/unit/rbeapi/api/system/default_spec.rb +5 -0
  50. data/spec/unit/rbeapi/api/system/fixture_system.text +1 -0
  51. data/spec/unit/rbeapi/api/vlans/default_spec.rb +30 -0
  52. data/spec/unit/rbeapi/api/vrrp/default_spec.rb +10 -0
  53. data/spec/unit/rbeapi/client_spec.rb +42 -0
  54. data/spec/unit/rbeapi/switchconfig2_spec.rb +119 -0
  55. data/spec/unit/rbeapi/switchconfig3_spec.rb +125 -0
  56. data/spec/unit/rbeapi/switchconfig_spec.rb +335 -0
  57. metadata +21 -7
@@ -43,22 +43,29 @@ module Rbeapi
43
43
  #
44
44
  class Prefixlists < Entity
45
45
  ##
46
- # Returns the static routes configured on the node.
46
+ # Returns the prefix list configured on the node.
47
47
  #
48
48
  # @example
49
- # {
50
- # <route>: {
51
- # next_hop: <string>,
52
- # name: <string, nil>
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<String, String> The method will return all of the
59
- # configured static routes on the node as a Ruby hash object. If
60
- # there are no static routes configured, this method will return
61
- # an empty hash.
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
- 'prefex' => entry[2] }
76
+ 'prefix' => entry[2] }
70
77
  end
71
78
  end
72
79
 
73
80
  ##
74
- # Returns the static routes configured on the node.
81
+ # Returns all prefix lists configured on the node.
75
82
  #
76
83
  # @example
77
84
  # {
78
- # <route>: {
79
- # next_hop: <string>,
80
- # name: <string, nil>
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, String> The method will return all of the
85
- # configured static routes on the node as a Ruby hash object. If
86
- # there are no static routes configured, this method will return
87
- # an empty hash.
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 "ip prefix-list #{name}"
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 cmd
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 cmd
167
+ configure(cmd)
138
168
  end
139
169
  end
140
170
  end
@@ -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|
@@ -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. If no instances are configured
200
- # then this method will return an empty array.
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
- if /-/ !~ vlan
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 ots value [Array] The list of vlan ids to configure on the
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 = ['switchport trunk allowed vlan none',
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)
@@ -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/.match(config)
95
+ mdata = /no\sip\srouting$/.match(config)
96
96
  { iprouting: mdata.nil? ? true : false }
97
97
  end
98
98
  private :parse_iprouting
@@ -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
@@ -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.
@@ -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
@@ -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
- result = run_commands("show #{config} #{params}", encoding: 'text')
515
- return result.first['output'].strip.split("\n") unless as_string
516
- result.first['output'].strip
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