cisco_node_utils 1.0.1 → 1.1.0

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