cisco_node_utils 1.0.1 → 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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rubocop.yml +81 -1
  4. data/.travis.yml +9 -0
  5. data/CHANGELOG.md +72 -6
  6. data/CONTRIBUTING.md +32 -7
  7. data/README.md +70 -7
  8. data/Rakefile +17 -0
  9. data/bin/check_metric_limits.rb +109 -0
  10. data/bin/git/hooks/commit-msg/enforce_style +81 -0
  11. data/bin/git/hooks/hook_lib +108 -0
  12. data/bin/git/hooks/hooks-wrapper +38 -0
  13. data/bin/git/hooks/post-flow-hotfix-start/update-version +24 -0
  14. data/bin/git/hooks/post-flow-release-finish/update-version +29 -0
  15. data/bin/git/hooks/post-flow-release-start/update-version +19 -0
  16. data/bin/git/hooks/post-merge/update-hooks +6 -0
  17. data/bin/git/hooks/post-rewrite/update-hooks +6 -0
  18. data/bin/git/hooks/pre-commit/rubocop +20 -0
  19. data/bin/git/hooks/pre-commit/validate-diffs +31 -0
  20. data/bin/git/hooks/pre-push/check-changelog +24 -0
  21. data/bin/git/hooks/pre-push/rubocop +7 -0
  22. data/bin/git/update-hooks +65 -0
  23. data/cisco_node_utils.gemspec +9 -3
  24. data/docs/README-develop-best-practices.md +404 -0
  25. data/docs/README-develop-node-utils-APIs.md +215 -365
  26. data/docs/README-maintainers.md +33 -3
  27. data/docs/template-router.rb +89 -91
  28. data/docs/template-test_router.rb +52 -55
  29. data/lib/.rubocop.yml +18 -0
  30. data/lib/cisco_node_utils.rb +2 -19
  31. data/lib/cisco_node_utils/README_YAML.md +1 -9
  32. data/lib/cisco_node_utils/bgp.rb +664 -0
  33. data/lib/cisco_node_utils/bgp_af.rb +530 -0
  34. data/lib/cisco_node_utils/bgp_neighbor.rb +425 -0
  35. data/lib/cisco_node_utils/bgp_neighbor_af.rb +709 -0
  36. data/lib/cisco_node_utils/cisco_cmn_utils.rb +59 -25
  37. data/lib/cisco_node_utils/command_reference.rb +72 -74
  38. data/lib/cisco_node_utils/command_reference_common.yaml +174 -9
  39. data/lib/cisco_node_utils/command_reference_common_bgp.yaml +535 -0
  40. data/lib/cisco_node_utils/command_reference_n7k.yaml +4 -0
  41. data/lib/cisco_node_utils/command_reference_n9k.yaml +0 -9
  42. data/lib/cisco_node_utils/configparser_lib.rb +152 -147
  43. data/lib/cisco_node_utils/dns_domain.rb +79 -0
  44. data/lib/cisco_node_utils/domain_name.rb +71 -0
  45. data/lib/cisco_node_utils/interface.rb +167 -161
  46. data/lib/cisco_node_utils/interface_ospf.rb +78 -81
  47. data/lib/cisco_node_utils/name_server.rb +64 -0
  48. data/lib/cisco_node_utils/node.rb +154 -198
  49. data/lib/cisco_node_utils/node_util.rb +61 -0
  50. data/lib/cisco_node_utils/ntp_config.rb +65 -0
  51. data/lib/cisco_node_utils/ntp_server.rb +76 -0
  52. data/lib/cisco_node_utils/platform.rb +174 -165
  53. data/lib/cisco_node_utils/radius_global.rb +146 -0
  54. data/lib/cisco_node_utils/radius_server.rb +295 -0
  55. data/lib/cisco_node_utils/router_ospf.rb +59 -63
  56. data/lib/cisco_node_utils/router_ospf_vrf.rb +226 -210
  57. data/lib/cisco_node_utils/snmpcommunity.rb +52 -58
  58. data/lib/cisco_node_utils/snmpgroup.rb +22 -23
  59. data/lib/cisco_node_utils/snmpserver.rb +99 -103
  60. data/lib/cisco_node_utils/snmpuser.rb +294 -274
  61. data/lib/cisco_node_utils/syslog_server.rb +92 -0
  62. data/lib/cisco_node_utils/syslog_settings.rb +69 -0
  63. data/lib/cisco_node_utils/tacacs_server.rb +137 -133
  64. data/lib/cisco_node_utils/tacacs_server_host.rb +84 -87
  65. data/lib/cisco_node_utils/version.rb +2 -1
  66. data/lib/cisco_node_utils/vlan.rb +28 -31
  67. data/lib/cisco_node_utils/vrf.rb +80 -0
  68. data/lib/cisco_node_utils/vtp.rb +100 -97
  69. data/lib/cisco_node_utils/yum.rb +15 -17
  70. data/tests/.rubocop.yml +15 -0
  71. data/tests/basetest.rb +81 -36
  72. data/tests/ciscotest.rb +38 -78
  73. data/{lib/cisco_node_utils → tests}/platform_info.rb +12 -8
  74. data/{lib/cisco_node_utils → tests}/platform_info.yaml +1 -1
  75. data/tests/test_bgp_af.rb +920 -0
  76. data/tests/test_bgp_neighbor.rb +403 -0
  77. data/tests/test_bgp_neighbor_af.rb +589 -0
  78. data/tests/test_command_config.rb +65 -62
  79. data/tests/test_command_reference.rb +31 -45
  80. data/tests/test_dns_domain.rb +113 -0
  81. data/tests/test_domain_name.rb +86 -0
  82. data/tests/test_interface.rb +424 -548
  83. data/tests/test_interface_ospf.rb +248 -432
  84. data/tests/test_interface_svi.rb +56 -79
  85. data/tests/test_interface_switchport.rb +196 -272
  86. data/tests/test_name_server.rb +85 -0
  87. data/tests/test_node.rb +7 -6
  88. data/tests/test_node_ext.rb +133 -186
  89. data/tests/test_ntp_config.rb +49 -0
  90. data/tests/test_ntp_server.rb +74 -0
  91. data/tests/test_platform.rb +58 -37
  92. data/tests/test_radius_global.rb +78 -0
  93. data/tests/test_radius_server.rb +185 -0
  94. data/tests/test_router_bgp.rb +838 -0
  95. data/tests/test_router_ospf.rb +49 -80
  96. data/tests/test_router_ospf_vrf.rb +274 -392
  97. data/tests/test_snmpcommunity.rb +128 -172
  98. data/tests/test_snmpgroup.rb +12 -14
  99. data/tests/test_snmpserver.rb +160 -189
  100. data/tests/test_snmpuser.rb +568 -717
  101. data/tests/test_syslog_server.rb +88 -0
  102. data/tests/test_syslog_settings.rb +54 -0
  103. data/tests/test_tacacs_server.rb +113 -148
  104. data/tests/test_tacacs_server_host.rb +108 -161
  105. data/tests/test_vlan.rb +63 -79
  106. data/tests/test_vrf.rb +92 -0
  107. data/tests/test_vtp.rb +108 -126
  108. data/tests/test_yum.rb +47 -41
  109. metadata +92 -56
  110. data/.rubocop_todo.yml +0 -293
  111. data/docs/.rubocop.yml +0 -13
  112. data/docs/template-feature.rb +0 -45
  113. data/docs/template-test_feature.rb +0 -51
  114. data/tests/test_all_cisco.rb +0 -46
@@ -1,6 +1,3 @@
1
- #
2
- # NXAPI implementation of Interface OSPF class
3
- #
4
1
  # March 2015, Alex Hunsberger
5
2
  #
6
3
  # Copyright (c) 2015 Cisco and/or its affiliates.
@@ -18,58 +15,56 @@
18
15
  # limitations under the License.
19
16
 
20
17
  require 'ipaddr'
21
- require File.join(File.dirname(__FILE__), 'node')
22
- require File.join(File.dirname(__FILE__), 'interface')
18
+ require_relative 'node_util'
19
+ require_relative 'interface'
23
20
  # Interestingly enough, interface OSPF configuration can exist completely
24
21
  # independent of router OSPF configuration... so we don't need RouterOspf here.
25
22
 
26
23
  module Cisco
27
- class InterfaceOspf
24
+ # InterfaceOspf - node utility class for per-interface OSPF config management
25
+ class InterfaceOspf < NodeUtil
28
26
  attr_reader :interface, :ospf_name
29
27
 
30
- @@node = Node.instance
31
-
32
28
  def initialize(int_name, ospf_name, area, create=true)
33
- raise TypeError unless int_name.is_a? String
34
- raise TypeError unless ospf_name.is_a? String
35
- raise TypeError unless area.is_a? String
36
- raise ArgumentError unless int_name.length > 0
37
- raise ArgumentError unless ospf_name.length > 0
38
- raise ArgumentError unless area.length > 0
29
+ fail TypeError unless int_name.is_a? String
30
+ fail TypeError unless ospf_name.is_a? String
31
+ fail TypeError unless area.is_a? String
32
+ fail ArgumentError unless int_name.length > 0
33
+ fail ArgumentError unless ospf_name.length > 0
34
+ fail ArgumentError unless area.length > 0
39
35
 
40
36
  # normalize
41
37
  int_name = int_name.downcase
42
38
  @interface = Interface.interfaces[int_name]
43
- raise "interface #{int_name} does not exist" if @interface.nil?
39
+ fail "interface #{int_name} does not exist" if @interface.nil?
44
40
 
45
41
  @ospf_name = ospf_name
46
42
 
47
- if create
48
- # enable feature ospf if it isn't
49
- RouterOspf.enable unless RouterOspf.enabled
43
+ return unless create
44
+ # enable feature ospf if it isn't
45
+ RouterOspf.enable unless RouterOspf.enabled
50
46
 
51
- @@node.config_set("interface_ospf", "area", @interface.name,
52
- "", @ospf_name, area)
53
- end
47
+ config_set('interface_ospf', 'area', @interface.name,
48
+ '', @ospf_name, area)
54
49
  end
55
50
 
56
51
  # can't re-use Interface.interfaces because we need to filter based on
57
52
  # "ip router ospf <name>", which Interface doesn't retrieve
58
- def InterfaceOspf.interfaces(ospf_name=nil)
59
- raise TypeError unless ospf_name.is_a? String or ospf_name.nil?
53
+ def self.interfaces(ospf_name=nil)
54
+ fail TypeError unless ospf_name.is_a?(String) || ospf_name.nil?
60
55
  ints = {}
61
56
 
62
- intf_list = @@node.config_get("interface", "all_interfaces")
57
+ intf_list = config_get('interface', 'all_interfaces')
63
58
  return ints if intf_list.nil?
64
59
  intf_list.each do |name|
65
- match = @@node.config_get("interface_ospf", "area", name)
60
+ match = config_get('interface_ospf', 'area', name)
66
61
  next if match.nil?
67
62
  # should only be a single match under a given interface
68
63
  match = match.first
69
64
  # ip router ospf <name> area <area>
70
65
  ospf = match[0]
71
66
  area = match[1]
72
- next unless ospf_name.nil? or ospf == ospf_name
67
+ next unless ospf_name.nil? || ospf == ospf_name
73
68
  int = name.downcase
74
69
  ints[int] = InterfaceOspf.new(int, ospf, area, false)
75
70
  end
@@ -77,7 +72,7 @@ module Cisco
77
72
  end
78
73
 
79
74
  def area
80
- match = @@node.config_get("interface_ospf", "area", @interface.name)
75
+ match = config_get('interface_ospf', 'area', @interface.name)
81
76
  return nil if match.nil?
82
77
  val = match[0][1]
83
78
  # Coerce numeric area to the expected dot-decimal format.
@@ -86,79 +81,81 @@ module Cisco
86
81
  end
87
82
 
88
83
  def area=(a)
89
- @@node.config_set("interface_ospf", "area", @interface.name,
90
- "", @ospf_name, a)
84
+ config_set('interface_ospf', 'area', @interface.name,
85
+ '', @ospf_name, a)
91
86
  end
92
87
 
93
88
  def destroy
94
- @@node.config_set("interface_ospf", "area", @interface.name,
95
- "no", @ospf_name, area)
89
+ config_set('interface_ospf', 'area', @interface.name,
90
+ 'no', @ospf_name, area)
96
91
  # Reset everything else back to default as well:
97
92
  self.message_digest = default_message_digest
98
- message_digest_key_set(default_message_digest_key_id, "", "", "")
93
+ message_digest_key_set(default_message_digest_key_id, '', '', '')
99
94
  self.cost = default_cost
100
95
  self.hello_interval = default_hello_interval
101
- @@node.config_set("interface_ospf", "dead_interval",
102
- @interface.name, "no", "")
96
+ config_set('interface_ospf', 'dead_interval',
97
+ @interface.name, 'no', '')
103
98
  self.passive_interface = default_passive_interface if passive_interface
104
99
  end
105
100
 
106
101
  def default_message_digest
107
- @@node.config_get_default("interface_ospf", "message_digest")
102
+ config_get_default('interface_ospf', 'message_digest')
108
103
  end
109
104
 
110
105
  def message_digest
111
- not @@node.config_get("interface_ospf", "message_digest",
112
- @interface.name).nil?
106
+ !config_get('interface_ospf', 'message_digest',
107
+ @interface.name).nil?
113
108
  end
114
109
 
115
110
  # interface %s
116
111
  # %s ip ospf authentication message-digest
117
112
  def message_digest=(enable)
118
- @@node.config_set("interface_ospf", "message_digest", @interface.name,
119
- enable ? "" : "no")
113
+ config_set('interface_ospf', 'message_digest', @interface.name,
114
+ enable ? '' : 'no')
120
115
  end
121
116
 
122
117
  def default_message_digest_key_id
123
- @@node.config_get_default("interface_ospf", "message_digest_key_id")
118
+ config_get_default('interface_ospf', 'message_digest_key_id')
124
119
  end
125
120
 
126
121
  def message_digest_key_id
127
- match = @@node.config_get("interface_ospf", "message_digest_key_id",
128
- @interface.name)
122
+ match = config_get('interface_ospf', 'message_digest_key_id',
123
+ @interface.name)
129
124
  # regex in yaml returns an array result, use .first to get match
130
125
  match.nil? ? default_message_digest_key_id : match.first.to_i
131
126
  end
132
127
 
133
128
  def default_message_digest_algorithm_type
134
- @@node.config_get_default("interface_ospf",
135
- "message_digest_alg_type").to_sym
129
+ config_get_default('interface_ospf',
130
+ 'message_digest_alg_type').to_sym
136
131
  end
137
132
 
138
133
  def message_digest_algorithm_type
139
- match = @@node.config_get("interface_ospf", "message_digest_alg_type",
140
- @interface.name)
134
+ match = config_get('interface_ospf', 'message_digest_alg_type',
135
+ @interface.name)
141
136
  # regex in yaml returns an array result, use .first to get match
142
- match.nil? ? default_message_digest_algorithm_type :
143
- match.first.to_sym
137
+ match.nil? ? default_message_digest_algorithm_type : match.first.to_sym
144
138
  end
145
139
 
146
140
  def default_message_digest_encryption_type
147
141
  Encryption.cli_to_symbol(
148
- @@node.config_get_default("interface_ospf", "message_digest_enc_type"))
142
+ config_get_default('interface_ospf', 'message_digest_enc_type'))
149
143
  end
150
144
 
151
145
  def message_digest_encryption_type
152
- match = @@node.config_get("interface_ospf", "message_digest_enc_type",
153
- @interface.name)
146
+ match = config_get('interface_ospf', 'message_digest_enc_type',
147
+ @interface.name)
154
148
  # regex in yaml returns an array result, use .first to get match
155
- match.nil? ? default_message_digest_encryption_type :
149
+ if match.nil?
150
+ default_message_digest_encryption_type
151
+ else
156
152
  Encryption.cli_to_symbol(match.first)
153
+ end
157
154
  end
158
155
 
159
156
  def message_digest_password
160
- match = @@node.config_get("interface_ospf", "message_digest_password",
161
- @interface.name)
157
+ match = config_get('interface_ospf', 'message_digest_password',
158
+ @interface.name)
162
159
  match.nil? ? nil : match.first
163
160
  end
164
161
 
@@ -167,89 +164,89 @@ module Cisco
167
164
  def message_digest_key_set(keyid, algtype, enctype, enc)
168
165
  current_keyid = message_digest_key_id
169
166
  if keyid == default_message_digest_key_id && current_keyid != keyid
170
- @@node.config_set("interface_ospf", "message_digest_key_set",
171
- @interface.name, "no", current_keyid,
172
- "", "", "")
167
+ config_set('interface_ospf', 'message_digest_key_set',
168
+ @interface.name, 'no', current_keyid,
169
+ '', '', '')
173
170
  elsif keyid != default_message_digest_key_id
174
- raise TypeError unless enc.is_a?(String)
175
- raise ArgumentError unless enc.length > 0
171
+ fail TypeError unless enc.is_a?(String)
172
+ fail ArgumentError unless enc.length > 0
176
173
  enctype = Encryption.symbol_to_cli(enctype)
177
- @@node.config_set("interface_ospf", "message_digest_key_set",
178
- @interface.name, "", keyid, algtype, enctype, enc)
174
+ config_set('interface_ospf', 'message_digest_key_set',
175
+ @interface.name, '', keyid, algtype, enctype, enc)
179
176
  end
180
177
  end
181
178
 
182
179
  def cost
183
- match = @@node.config_get("interface_ospf", "cost", @interface.name)
180
+ match = config_get('interface_ospf', 'cost', @interface.name)
184
181
  # regex in yaml returns an array result, use .first to get match
185
182
  match.nil? ? default_cost : match.first.to_i
186
183
  end
187
184
 
188
185
  def default_cost
189
- @@node.config_get_default("interface_ospf", "cost")
186
+ config_get_default('interface_ospf', 'cost')
190
187
  end
191
188
 
192
189
  # interface %s
193
190
  # ip ospf cost %d
194
191
  def cost=(c)
195
192
  if c == default_cost
196
- @@node.config_set("interface_ospf", "cost", @interface.name, "no", "")
193
+ config_set('interface_ospf', 'cost', @interface.name, 'no', '')
197
194
  else
198
- @@node.config_set("interface_ospf", "cost", @interface.name, "", c)
195
+ config_set('interface_ospf', 'cost', @interface.name, '', c)
199
196
  end
200
197
  end
201
198
 
202
199
  def hello_interval
203
- match = @@node.config_get("interface_ospf", "hello_interval",
204
- @interface.name)
200
+ match = config_get('interface_ospf', 'hello_interval',
201
+ @interface.name)
205
202
  # regex in yaml returns an array result, use .first to get match
206
203
  match.nil? ? default_hello_interval : match.first.to_i
207
204
  end
208
205
 
209
206
  def default_hello_interval
210
- @@node.config_get_default("interface_ospf", "hello_interval")
207
+ config_get_default('interface_ospf', 'hello_interval')
211
208
  end
212
209
 
213
210
  # interface %s
214
211
  # ip ospf hello-interval %d
215
212
  def hello_interval=(interval)
216
- @@node.config_set("interface_ospf", "hello_interval",
217
- @interface.name, "", interval.to_i)
213
+ config_set('interface_ospf', 'hello_interval',
214
+ @interface.name, '', interval.to_i)
218
215
  end
219
216
 
220
217
  def dead_interval
221
- match = @@node.config_get("interface_ospf", "dead_interval",
222
- @interface.name)
218
+ match = config_get('interface_ospf', 'dead_interval',
219
+ @interface.name)
223
220
  # regex in yaml returns an array result, use .first to get match
224
221
  match.nil? ? default_dead_interval : match.first.to_i
225
222
  end
226
223
 
227
224
  def default_dead_interval
228
- @@node.config_get_default("interface_ospf", "dead_interval")
225
+ config_get_default('interface_ospf', 'dead_interval')
229
226
  end
230
227
 
231
228
  # interface %s
232
229
  # ip ospf dead-interval %d
233
230
  def dead_interval=(interval)
234
- @@node.config_set("interface_ospf", "dead_interval",
235
- @interface.name, "", interval.to_i)
231
+ config_set('interface_ospf', 'dead_interval',
232
+ @interface.name, '', interval.to_i)
236
233
  end
237
234
 
238
235
  def default_passive_interface
239
- @@node.config_get_default("interface_ospf", "passive_interface")
236
+ config_get_default('interface_ospf', 'passive_interface')
240
237
  end
241
238
 
242
239
  def passive_interface
243
- not @@node.config_get("interface_ospf", "passive_interface",
244
- @interface.name).nil?
240
+ !config_get('interface_ospf', 'passive_interface',
241
+ @interface.name).nil?
245
242
  end
246
243
 
247
244
  # interface %s
248
245
  # %s ip ospf passive-interface
249
246
  def passive_interface=(enable)
250
- raise TypeError unless enable == true or enable == false
251
- @@node.config_set("interface_ospf", "passive_interface", @interface.name,
252
- enable ? "" : "no")
247
+ fail TypeError unless enable == true || enable == false
248
+ config_set('interface_ospf', 'passive_interface', @interface.name,
249
+ enable ? '' : 'no')
253
250
  end
254
251
  end
255
252
  end
@@ -0,0 +1,64 @@
1
+ #
2
+ # NXAPI implementation of NameServer class
3
+ #
4
+ # September 2015, Hunter Haugen
5
+ #
6
+ # Copyright (c) 2015 Cisco and/or its affiliates.
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+ # "group" is a standard SNMP term but in NXOS "role" is used to serve the
21
+ # purpose of group; thus this provider utility does not create snmp groups
22
+ # and is limited to reporting group (role) existence only.
23
+
24
+ require_relative 'node_util'
25
+
26
+ module Cisco
27
+ # NameServer - node utility class for DNS client name server config management
28
+ class NameServer < NodeUtil
29
+ attr_reader :name
30
+
31
+ def initialize(name, instantiate=true)
32
+ unless name.is_a? String
33
+ fail TypeError, "Expected a string, got a #{name.inspect}"
34
+ end
35
+ @name = name
36
+ create if instantiate
37
+ end
38
+
39
+ def self.nameservers
40
+ hosts = config_get('dnsclient', 'name_server')
41
+ return {} if hosts.nil?
42
+
43
+ hash = {}
44
+ # Join and split because config_get returns array of strings separated by
45
+ # spaces (regexes are a subset of PDA)
46
+ hosts.join(' ').split(' ').each do |name|
47
+ hash[name] = NameServer.new(name, false)
48
+ end
49
+ hash
50
+ end
51
+
52
+ def ==(other)
53
+ name == other.name
54
+ end
55
+
56
+ def create
57
+ config_set('dnsclient', 'name_server', state: '', ip: @name)
58
+ end
59
+
60
+ def destroy
61
+ config_set('dnsclient', 'name_server', state: 'no', ip: @name)
62
+ end
63
+ end
64
+ end
@@ -21,8 +21,9 @@
21
21
  require 'singleton'
22
22
 
23
23
  require 'cisco_nxapi'
24
- require File.join(File.dirname(__FILE__), 'command_reference')
24
+ require_relative 'command_reference'
25
25
 
26
+ # Add node management classes and APIs to the Cisco namespace.
26
27
  module Cisco
27
28
  # Error class raised by the config_set and config_get APIs if the
28
29
  # device encounters an issue trying to act on the requested CLI.
@@ -33,7 +34,7 @@ module Cisco
33
34
  attr_reader :command, :clierror, :previous
34
35
  def initialize(command, clierror, previous)
35
36
  @command = command
36
- @clierror = clierror.rstrip
37
+ @clierror = clierror.rstrip if clierror.kind_of? String
37
38
  @previous = previous
38
39
  end
39
40
 
@@ -46,7 +47,6 @@ module Cisco
46
47
  # Singleton representing the network node (switch/router) that is
47
48
  # running this code. The singleton is lazily instantiated, meaning that
48
49
  # it doesn't exist until some client requests it (with Node.instance())
49
-
50
50
  class Node
51
51
  include Singleton
52
52
 
@@ -61,14 +61,54 @@ module Cisco
61
61
  #
62
62
  # @raise [IndexError] if the given (feature, name) pair is not in the
63
63
  # CommandReference data or if the data doesn't have values defined
64
- # for the 'config_get' and 'config_get_token' fields.
64
+ # for the 'config_get' and (optional) 'config_get_token' fields.
65
65
  # @raise [Cisco::CliError] if the given command is rejected by the device.
66
66
  #
67
67
  # @param feature [String]
68
68
  # @param name [String]
69
- # @return [String]
69
+ # @return [String, Hash, Array]
70
70
  # @example config_get("show_version", "system_image")
71
- def config_get(feature, name)
71
+ # @example config_get("ospf", "router_id",
72
+ # {:name => "green", :vrf => "one"})
73
+ def config_get(feature, name, *args)
74
+ fail 'lazy_connect specified but did not request connect' unless @cmd_ref
75
+ ref = @cmd_ref.lookup(feature, name)
76
+
77
+ begin
78
+ token = build_config_get_token(feature, ref, args)
79
+ rescue IndexError, TypeError
80
+ # IndexError if value is not set, TypeError if set to nil explicitly
81
+ token = nil
82
+ end
83
+ if token.kind_of?(String)
84
+ if token[0] == '/' && token[-1] == '/'
85
+ fail RuntimeError unless args.length == token.scan(/%/).length
86
+ # convert string to regexp and replace %s with args
87
+ token = Regexp.new(sprintf(token, *args)[1..-2])
88
+ text = build_config_get(feature, ref, :ascii)
89
+ return Cisco.find_ascii(text, token)
90
+ else
91
+ hash = build_config_get(feature, ref, :structured)
92
+ return hash[token]
93
+ end
94
+ elsif token.kind_of?(Array)
95
+ # Array of /regexps/ -> ascii, array of strings/ints -> structured
96
+ if token[0].kind_of?(String) &&
97
+ token[0][0] == '/' &&
98
+ (token[0][-1] == '/' || token[0][-2..-1] == '/i')
99
+
100
+ token = token_str_to_regexp(token, args)
101
+ text = build_config_get(feature, ref, :ascii)
102
+ return Cisco.find_ascii(text, token[-1], *token[0..-2])
103
+
104
+ else
105
+ result = build_config_get(feature, ref, :structured)
106
+ return config_get_handle_structured(token, result)
107
+ end
108
+ elsif token.nil?
109
+ return show(ref.config_get, :structured)
110
+ end
111
+ fail TypeError("Unclear to handle config_get_token #{token}")
72
112
  end
73
113
 
74
114
  # Uses CommandReference to lookup the default value for a given
@@ -82,6 +122,9 @@ module Cisco
82
122
  # @return [String]
83
123
  # @example config_get_default("vtp", "file")
84
124
  def config_get_default(feature, name)
125
+ fail 'lazy_connect specified but did not request connect' unless @cmd_ref
126
+ ref = @cmd_ref.lookup(feature, name)
127
+ ref.default_value
85
128
  end
86
129
 
87
130
  # Uses CommandReference to look up the given config command(s) of interest
@@ -95,7 +138,37 @@ module Cisco
95
138
  # @param name [String]
96
139
  # @param args [*String] zero or more args to be substituted into the cmdref.
97
140
  # @example config_set("vtp", "domain", "example.com")
141
+ # @example config_set("ospf", "router_id",
142
+ # {:name => "green", :vrf => "one", :state => "",
143
+ # :router_id => "192.0.0.1"})
98
144
  def config_set(feature, name, *args)
145
+ fail 'lazy_connect specified but did not request connect' unless @cmd_ref
146
+ ref = @cmd_ref.lookup(feature, name)
147
+ config_set = build_config_set(feature, ref, args)
148
+ if config_set.is_a?(String)
149
+ param_count = config_set.scan(/%/).length
150
+ elsif config_set.is_a?(Array)
151
+ param_count = config_set.join(' ').scan(/%/).length
152
+ else
153
+ fail TypeError, '%{config_set.class} not supported for config_set'
154
+ end
155
+ unless args[0].is_a? Hash
156
+ if param_count != args.length
157
+ fail ArgumentError, 'Wrong number of params - expected: ' \
158
+ "#{param_count} actual: #{args.length}"
159
+ end
160
+ end
161
+ if config_set.is_a?(String)
162
+ config(sprintf(config_set, *args))
163
+ elsif config_set.is_a?(Array)
164
+ new_config_set = []
165
+ config_set.each do |line|
166
+ param_count = line.scan(/%/).length
167
+ new_config_set << sprintf(line, *args.first(param_count))
168
+ args = args[param_count..-1]
169
+ end
170
+ config(new_config_set)
171
+ end
99
172
  end
100
173
 
101
174
  # Clear the cache of CLI output results.
@@ -104,25 +177,29 @@ module Cisco
104
177
  # whenever a config_set() is called, but providers may also call this
105
178
  # to explicitly force the cache to be cleared.
106
179
  def cache_flush
180
+ @client.cache_flush
107
181
  end
108
182
 
109
- # END NODE API
110
183
  # Here and below are implementation details and private APIs that most
111
184
  # providers shouldn't need to know about or use.
112
185
 
113
186
  attr_reader :cmd_ref, :client
114
187
 
115
188
  # For unit testing - we won't know the node connection info at load time.
116
- @@lazy_connect = false
189
+ @lazy_connect = false
190
+
191
+ class << self
192
+ attr_reader :lazy_connect
193
+ end
117
194
 
118
- def Node.lazy_connect=(val)
119
- @@lazy_connect = val
195
+ class << self
196
+ attr_writer :lazy_connect
120
197
  end
121
198
 
122
199
  def initialize
123
200
  @client = nil
124
201
  @cmd_ref = nil
125
- connect unless @@lazy_connect
202
+ connect unless self.class.lazy_connect
126
203
  end
127
204
 
128
205
  def to_s
@@ -141,13 +218,6 @@ module Cisco
141
218
  @client.reload
142
219
  end
143
220
 
144
- # hidden as well
145
- attr_reader :client
146
-
147
- def cache_flush
148
- @client.cache_flush
149
- end
150
-
151
221
  def cache_enable?
152
222
  @client.cache_enable?
153
223
  end
@@ -175,19 +245,19 @@ module Cisco
175
245
  def token_str_to_regexp(token, args)
176
246
  unless args[0].is_a? Hash
177
247
  expected_args = token.join.scan(/%/).length
178
- raise "Given #{args.length} args, but token #{token} requires " +
248
+ fail "Given #{args.length} args, but token #{token} requires " \
179
249
  "#{expected_args}" unless args.length == expected_args
180
250
  end
181
251
  # replace all %s with *args
182
252
  token.map! { |str| sprintf(str, *args.shift(str.scan(/%/).length)) }
183
253
  # convert all to Regexp objects
184
- token.map! { |str|
254
+ token.map! do |str|
185
255
  if str[-2..-1] == '/i'
186
256
  Regexp.new(str[1..-3], Regexp::IGNORECASE)
187
257
  else
188
258
  Regexp.new(str[1..-2])
189
259
  end
190
- }
260
+ end
191
261
  token
192
262
  end
193
263
 
@@ -210,7 +280,7 @@ module Cisco
210
280
  replace = regexp.scan(/<(\S+)>/).flatten.map(&:to_sym)
211
281
  replace.each do |item|
212
282
  regexp = regexp.sub "<#{item}>",
213
- values[item].to_s if values.key?(item)
283
+ values[item].to_s if values.key?(item)
214
284
  end
215
285
  # Only return lines that actually replaced ids or did not have any
216
286
  # ids to replace. Implicit nil returned if not.
@@ -235,7 +305,7 @@ module Cisco
235
305
  # @param ref [CommandReference::CmdRef]
236
306
  # @return [String, Array]
237
307
  def build_config_get_token(feature, ref, args)
238
- raise "lazy_connect specified but did not request connect" unless @cmd_ref
308
+ fail 'lazy_connect specified but did not request connect' unless @cmd_ref
239
309
  # Why clone token? A bug in some ruby versions caused token to convert
240
310
  # to type Regexp unexpectedly. The clone hard copy resolved it.
241
311
 
@@ -247,7 +317,7 @@ module Cisco
247
317
  # Use _template yaml entry if config_get_token_append
248
318
  if ref.to_s[/config_get_token_append/]
249
319
  # Get yaml feature template:
250
- template = @cmd_ref.lookup(feature, "_template")
320
+ template = @cmd_ref.lookup(feature, '_template')
251
321
  # Process config_get_token: from template:
252
322
  token.push(replace_token_ids(template.config_get_token, options))
253
323
  # Process config_get_token_append sequence: from template:
@@ -272,13 +342,13 @@ module Cisco
272
342
  # @param type [Symbol]
273
343
  # @return [String, Array]
274
344
  def build_config_get(feature, ref, type)
275
- raise "lazy_connect specified but did not request connect" unless @cmd_ref
345
+ fail 'lazy_connect specified but did not request connect' unless @cmd_ref
276
346
  # Use feature name config_get string if present
277
347
  # else use feature template: config_get
278
- if ref.hash.key?("config_get")
348
+ if ref.hash.key?('config_get')
279
349
  return show(ref.config_get, type)
280
350
  else
281
- template = @cmd_ref.lookup(feature, "_template")
351
+ template = @cmd_ref.lookup(feature, '_template')
282
352
  return show(template.config_get, type)
283
353
  end
284
354
  end
@@ -291,7 +361,7 @@ module Cisco
291
361
  # @param ref [CommandReference::CmdRef]
292
362
  # @return [String, Array]
293
363
  def build_config_set(feature, ref, args)
294
- raise "lazy_connect specified but did not request connect" unless @cmd_ref
364
+ fail 'lazy_connect specified but did not request connect' unless @cmd_ref
295
365
  # If the options are presented as type Hash process as
296
366
  # key-value replacement pairs
297
367
  return ref.config_set unless args[0].is_a?(Hash)
@@ -300,7 +370,7 @@ module Cisco
300
370
  # Use _template yaml entry if config_set_append
301
371
  if ref.to_s[/config_set_append/]
302
372
  # Get yaml feature template:
303
- template = @cmd_ref.lookup(feature, "_template")
373
+ template = @cmd_ref.lookup(feature, '_template')
304
374
  # Process config_set: from template:
305
375
  config_set.push(replace_token_ids(template.config_set, options))
306
376
  # Process config_set_append sequence: from template:
@@ -317,146 +387,31 @@ module Cisco
317
387
  config_set
318
388
  end
319
389
 
320
- # Convenience wrapper for show(command, :structured).
321
- # Uses CommandReference to look up the given show command and key
322
- # of interest, executes that command, and returns the value corresponding
323
- # to that key.
324
- #
325
- # @raise [IndexError] if the given (feature, name) pair is not in the
326
- # CommandReference data or if the data doesn't have values defined
327
- # for the 'config_get' and (optional) 'config_get_token' fields.
328
- # @raise [Cisco::CliError] if the given command is rejected by the device.
329
- #
330
- # @param feature [String]
331
- # @param name [String]
332
- # @return [String, Hash, Array]
333
- # @example config_get("show_version", "system_image")
334
- # @example config_get("ospf", "router_id",
335
- # {:name => "green", :vrf => "one"})
336
- def config_get(feature, name, *args)
337
- raise "lazy_connect specified but did not request connect" unless @cmd_ref
338
- ref = @cmd_ref.lookup(feature, name)
339
-
340
- begin
341
- token = build_config_get_token(feature, ref, args)
342
- rescue IndexError, TypeError
343
- # IndexError if value is not set, TypeError if set to nil explicitly
344
- token = nil
345
- end
346
- if token.kind_of?(String) and token[0] == '/' and token[-1] == '/'
347
- raise RuntimeError unless args.length == token.scan(/%/).length
348
- # convert string to regexp and replace %s with args
349
- token = Regexp.new(sprintf(token, *args)[1..-2])
350
- text = build_config_get(feature, ref, :ascii)
351
- return Cisco.find_ascii(text, token)
352
- elsif token.kind_of?(String)
353
- hash = build_config_get(feature, ref, :structured)
354
- return hash[token]
355
-
356
- elsif token.kind_of?(Array)
357
- # Array of /regexps/ -> ascii, array of strings/ints -> structured
358
- if token[0].kind_of?(String) and
359
- token[0][0] == '/' and
360
- (token[0][-1] == '/' or token[0][-2..-1] == '/i')
361
-
362
- token = token_str_to_regexp(token, args)
363
- text = build_config_get(feature, ref, :ascii)
364
- return Cisco.find_ascii(text, token[-1], *token[0..-2])
365
-
366
- else
367
- result = build_config_get(feature, ref, :structured)
368
- begin
369
- token.each do |token|
370
- # if token is a hash and result is an array, check each
371
- # array index (which should return another hash) to see if
372
- # it contains the matching key/value pairs specified in token,
373
- # and return the first match (or nil)
374
- if token.kind_of?(Hash)
375
- raise "Expected array, got #{result.class}" unless result.kind_of?(Array)
376
- result = result.select { |x| token.all? { |k, v| x[k] == v } }
377
- raise "Multiple matches found for #{token}" if result.length > 1
378
- raise "No match found for #{token}" if result.length == 0
379
- result = result[0]
380
- else # result is array or hash
381
- raise "No key \"#{token}\" in #{result}" if result[token].nil?
382
- result = result[token]
383
- end
384
- end
385
- return result
386
- rescue Exception => e
387
- # TODO: logging user story, Syslog isn't available here
388
- # Syslog.debug(e.message)
389
- return nil
390
- end
391
- end
392
- elsif token.nil?
393
- return show(ref.config_get, :structured)
394
- end
395
- raise TypeError("Unclear to handle config_get_token #{token}")
396
- end
397
-
398
- # Uses CommandReference to lookup the default value for a given
399
- # feature and feature property.
400
- #
401
- # @raise [IndexError] if the given (feature, name) pair is not in the
402
- # CommandReference data or if the data doesn't have values defined
403
- # for the 'default_value' field.
404
- # @param feature [String]
405
- # @param name [String]
406
- # @return [String]
407
- # @example config_get_default("vtp", "file")
408
- def config_get_default(feature, name)
409
- raise "lazy_connect specified but did not request connect" unless @cmd_ref
410
- ref = @cmd_ref.lookup(feature, name)
411
- ref.default_value
412
- end
413
-
414
- # Uses CommandReference to look up the given config command(s) of interest
415
- # and then applies the configuration.
416
- #
417
- # @raise [IndexError] if no relevant cmd_ref config_set exists
418
- # @raise [ArgumentError] if too many or too few args are provided.
419
- # @raise [Cisco::CliError] if any command is rejected by the device.
420
- #
421
- # @param feature [String]
422
- # @param name [String]
423
- # @param args [*String] zero or more args to be substituted into the cmdref.
424
- # @example config_set("vtp", "domain", "example.com")
425
- # @example config_set("ospf", "router_id",
426
- # {:name => "green", :vrf => "one", :state => "",
427
- # :router_id => "192.0.0.1"})
428
- def config_set(feature, name, *args)
429
- raise "lazy_connect specified but did not request connect" unless @cmd_ref
430
- ref = @cmd_ref.lookup(feature, name)
431
- config_set = build_config_set(feature, ref, args)
432
- if config_set.is_a?(String)
433
- param_count = config_set.scan(/%/).length
434
- elsif config_set.is_a?(Array)
435
- param_count = config_set.join(" ").scan(/%/).length
436
- else
437
- raise TypeError, "%{config_set.class} not supported for config_set"
438
- end
439
- unless args[0].is_a? Hash
440
- if param_count != args.length
441
- raise ArgumentError.new("Wrong number of params - expected: " +
442
- "#{param_count} actual: #{args.length}")
390
+ # Helper method for config_get().
391
+ # @param token [Array, Hash] lookup sequence
392
+ # @param result [Array, Hash] structured output from node
393
+ def config_get_handle_structured(token, result)
394
+ token.each do |t|
395
+ # if token is a hash and result is an array, check each
396
+ # array index (which should return another hash) to see if
397
+ # it contains the matching key/value pairs specified in token,
398
+ # and return the first match (or nil)
399
+ if t.kind_of?(Hash)
400
+ fail "Expected array, got #{result.class}" unless result.is_a? Array
401
+ result = result.select { |x| t.all? { |k, v| x[k] == v } }
402
+ fail "Multiple matches found for #{t}" if result.length > 1
403
+ fail "No match found for #{t}" if result.length == 0
404
+ result = result[0]
405
+ else # result is array or hash
406
+ fail "No key \"#{t}\" in #{result}" if result[t].nil?
407
+ result = result[t]
443
408
  end
444
409
  end
445
- if config_set.is_a?(String)
446
- config(sprintf(config_set, *args))
447
- elsif config_set.is_a?(Array)
448
- new_config_set = []
449
- config_set.each do |line|
450
- param_count = line.scan(/%/).length
451
- if param_count > 0
452
- new_config_set << sprintf(line, *args)
453
- args = args[param_count..-1]
454
- else
455
- new_config_set << line
456
- end
457
- end
458
- config(new_config_set)
459
- end
410
+ result
411
+ rescue RuntimeError
412
+ # TODO: logging user story, Syslog isn't available here
413
+ # Syslog.debug(e.message)
414
+ nil
460
415
  end
461
416
 
462
417
  # Send a config command to the device.
@@ -465,6 +420,7 @@ module Cisco
465
420
  #
466
421
  # @raise [Cisco::CliError] if any command is rejected by the device.
467
422
  def config(commands)
423
+ CiscoLogger.debug("CLI Sent to device: #{commands}")
468
424
  @client.config(commands)
469
425
  rescue CiscoNxapi::CliError => e
470
426
  raise Cisco::CliError.new(e.input, e.clierror, e.previous)
@@ -483,52 +439,52 @@ module Cisco
483
439
 
484
440
  # @return [String] such as "Cisco Nexus Operating System (NX-OS) Software"
485
441
  def os
486
- o = config_get("show_version", "header")
487
- raise "failed to retrieve operating system information" if o.nil?
488
- o.split("\n")[0]
442
+ o = config_get('show_version', 'header')
443
+ fail 'failed to retrieve operating system information' if o.nil?
444
+ o.split("\n")[0]
489
445
  end
490
446
 
491
447
  # @return [String] such as "6.0(2)U5(1) [build 6.0(2)U5(0.941)]"
492
448
  def os_version
493
- config_get("show_version", "version")
449
+ config_get('show_version', 'version')
494
450
  end
495
451
 
496
452
  # @return [String] such as "Nexus 3048 Chassis"
497
453
  def product_description
498
- config_get("show_version", "description")
454
+ config_get('show_version', 'description')
499
455
  end
500
456
 
501
457
  # @return [String] such as "N3K-C3048TP-1GE"
502
458
  def product_id
503
459
  if @cmd_ref
504
- return config_get("inventory", "productid")
460
+ return config_get('inventory', 'productid')
505
461
  else
506
462
  # We use this function to *find* the appropriate CommandReference
507
- entries = show("show inventory", :structured)
508
- return entries["TABLE_inv"]["ROW_inv"][0]["productid"]
463
+ entries = show('show inventory', :structured)
464
+ return entries['TABLE_inv']['ROW_inv'][0]['productid']
509
465
  end
510
466
  end
511
467
 
512
468
  # @return [String] such as "V01"
513
469
  def product_version_id
514
- config_get("inventory", "versionid")
470
+ config_get('inventory', 'versionid')
515
471
  end
516
472
 
517
473
  # @return [String] such as "FOC1722R0ET"
518
474
  def product_serial_number
519
- config_get("inventory", "serialnum")
475
+ config_get('inventory', 'serialnum')
520
476
  end
521
477
 
522
478
  # @return [String] such as "bxb-oa-n3k-7"
523
479
  def host_name
524
- config_get("show_version", "host_name")
480
+ config_get('show_version', 'host_name')
525
481
  end
526
482
 
527
483
  # @return [String] such as "example.com"
528
484
  def domain_name
529
- result = config_get("domain_name", "domain_name")
485
+ result = config_get('dnsclient', 'domain_name')
530
486
  if result.nil?
531
- return ""
487
+ return ''
532
488
  else
533
489
  return result[0]
534
490
  end
@@ -537,8 +493,8 @@ module Cisco
537
493
  # @return [Integer] System uptime, in seconds
538
494
  def system_uptime
539
495
  cache_flush
540
- t = config_get("show_system", "uptime")
541
- raise "failed to retrieve system uptime" if t.nil?
496
+ t = config_get('show_system', 'uptime')
497
+ fail 'failed to retrieve system uptime' if t.nil?
542
498
  t = t.shift
543
499
  # time units: t = ["0", "23", "15", "49"]
544
500
  t.map!(&:to_i)
@@ -548,8 +504,8 @@ module Cisco
548
504
 
549
505
  # @return [String] timestamp of last reset time
550
506
  def last_reset_time
551
- output = config_get("show_version", "last_reset_time")
552
- return "" if output.nil?
507
+ output = config_get('show_version', 'last_reset_time')
508
+ return '' if output.nil?
553
509
  # NX-OS may provide leading/trailing whitespace:
554
510
  # " Sat Oct 25 00:39:25 2014\n"
555
511
  # so be sure to strip() it down to the actual string.
@@ -558,26 +514,26 @@ module Cisco
558
514
 
559
515
  # @return [String] such as "Reset Requested by CLI command reload"
560
516
  def last_reset_reason
561
- config_get("show_version", "last_reset_reason")
517
+ config_get('show_version', 'last_reset_reason')
562
518
  end
563
519
 
564
520
  # @return [Float] combined user/kernel CPU utilization
565
521
  def system_cpu_utilization
566
- output = config_get("system", "resources")
567
- raise "failed to retrieve cpu utilization" if output.nil?
568
- output["cpu_state_user"].to_f + output["cpu_state_kernel"].to_f
522
+ output = config_get('system', 'resources')
523
+ fail 'failed to retrieve cpu utilization' if output.nil?
524
+ output['cpu_state_user'].to_f + output['cpu_state_kernel'].to_f
569
525
  end
570
526
 
571
527
  # @return [String] such as
572
528
  # "bootflash:///n3000-uk9-kickstart.6.0.2.U5.0.941.bin"
573
529
  def boot
574
- config_get("show_version", "boot_image")
530
+ config_get('show_version', 'boot_image')
575
531
  end
576
532
 
577
533
  # @return [String] such as
578
534
  # "bootflash:///n3000-uk9.6.0.2.U5.0.941.bin"
579
535
  def system
580
- config_get("show_version", "system_image")
536
+ config_get('show_version', 'system_image')
581
537
  end
582
538
  end
583
539
 
@@ -598,8 +554,8 @@ module Cisco
598
554
  # => 'example.com'
599
555
  def find_one_ascii(body, regex_query, *parent_cfg)
600
556
  matches = find_ascii(body, regex_query, *parent_cfg)
601
- return "" if matches.nil?
602
- raise RuntimeError if matches.length > 1
557
+ return '' if matches.nil?
558
+ fail RuntimeError if matches.length > 1
603
559
  matches[0]
604
560
  end
605
561
  module_function :find_one_ascii
@@ -623,11 +579,11 @@ module Cisco
623
579
  # bgp_afs = find_ascii(show_run_bgp, /^address-family (.*)/,
624
580
  # /^router bgp #{ASN}/)
625
581
  def find_ascii(body, regex_query, *parent_cfg)
626
- return nil if body.nil? or regex_query.nil?
582
+ return nil if body.nil? || regex_query.nil?
627
583
 
628
584
  # get subconfig
629
585
  parent_cfg.each { |p| body = find_subconfig(body, p) }
630
- if body.nil?
586
+ if body.nil? || body.empty?
631
587
  return nil
632
588
  else
633
589
  # find matches and return as array of String if it only does one
@@ -635,7 +591,7 @@ module Cisco
635
591
  match = body.split("\n").map { |s| s.scan(regex_query) }
636
592
  match = match.flatten(1)
637
593
  return nil if match.empty?
638
- match = match.flatten if match[0].is_a?(Array) and match[0].length == 1
594
+ match = match.flatten if match[0].is_a?(Array) && match[0].length == 1
639
595
  return match
640
596
  end
641
597
  end
@@ -649,16 +605,16 @@ module Cisco
649
605
  # @return [String, nil] the subsection of body, de-indented
650
606
  # appropriately, or nil if no such subsection exists.
651
607
  def find_subconfig(body, regex_query)
652
- return nil if body.nil? or regex_query.nil?
608
+ return nil if body.nil? || regex_query.nil?
653
609
 
654
610
  rows = body.split("\n")
655
611
  match_row_index = rows.index { |row| regex_query =~ row }
656
612
  return nil if match_row_index.nil?
657
613
 
658
- cur = match_row_index+1
614
+ cur = match_row_index + 1
659
615
  subconfig = []
660
616
 
661
- until (/\A\s+.*/ =~ rows[cur]).nil? or cur == rows.length
617
+ until (/\A\s+.*/ =~ rows[cur]).nil? || cur == rows.length
662
618
  subconfig << rows[cur]
663
619
  cur += 1
664
620
  end