rbeapi 0.5.1 → 1.0

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