cisco_node_utils 0.9.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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +3 -0
  4. data/.rubocop_todo.yml +293 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CONTRIBUTING.md +31 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +201 -0
  9. data/README.md +113 -0
  10. data/Rakefile +4 -0
  11. data/cisco_node_utils.gemspec +30 -0
  12. data/lib/cisco_node_utils.rb +33 -0
  13. data/lib/cisco_node_utils/README_YAML.md +333 -0
  14. data/lib/cisco_node_utils/cisco_cmn_utils.rb +92 -0
  15. data/lib/cisco_node_utils/command_reference.rb +415 -0
  16. data/lib/cisco_node_utils/command_reference_common.yaml +845 -0
  17. data/lib/cisco_node_utils/command_reference_n3064.yaml +13 -0
  18. data/lib/cisco_node_utils/command_reference_n7k.yaml +48 -0
  19. data/lib/cisco_node_utils/command_reference_n9k.yaml +35 -0
  20. data/lib/cisco_node_utils/configparser_lib.rb +196 -0
  21. data/lib/cisco_node_utils/interface.rb +501 -0
  22. data/lib/cisco_node_utils/interface_ospf.rb +241 -0
  23. data/lib/cisco_node_utils/node.rb +673 -0
  24. data/lib/cisco_node_utils/platform.rb +184 -0
  25. data/lib/cisco_node_utils/platform_info.rb +58 -0
  26. data/lib/cisco_node_utils/platform_info.yaml +10 -0
  27. data/lib/cisco_node_utils/router_ospf.rb +96 -0
  28. data/lib/cisco_node_utils/router_ospf_vrf.rb +258 -0
  29. data/lib/cisco_node_utils/snmpcommunity.rb +91 -0
  30. data/lib/cisco_node_utils/snmpgroup.rb +55 -0
  31. data/lib/cisco_node_utils/snmpserver.rb +150 -0
  32. data/lib/cisco_node_utils/snmpuser.rb +342 -0
  33. data/lib/cisco_node_utils/tacacs_server.rb +175 -0
  34. data/lib/cisco_node_utils/tacacs_server_host.rb +128 -0
  35. data/lib/cisco_node_utils/version.rb +17 -0
  36. data/lib/cisco_node_utils/vlan.rb +153 -0
  37. data/lib/cisco_node_utils/vtp.rb +127 -0
  38. data/lib/cisco_node_utils/yum.rb +84 -0
  39. data/tests/basetest.rb +93 -0
  40. data/tests/ciscotest.rb +136 -0
  41. data/tests/cmd_config.yaml +51 -0
  42. data/tests/cmd_config_invalid.yaml +16 -0
  43. data/tests/test_all_cisco.rb +46 -0
  44. data/tests/test_command_config.rb +192 -0
  45. data/tests/test_command_reference.rb +222 -0
  46. data/tests/test_interface.rb +1017 -0
  47. data/tests/test_interface_ospf.rb +763 -0
  48. data/tests/test_interface_svi.rb +267 -0
  49. data/tests/test_interface_switchport.rb +722 -0
  50. data/tests/test_node.rb +108 -0
  51. data/tests/test_node_ext.rb +450 -0
  52. data/tests/test_platform.rb +188 -0
  53. data/tests/test_router_ospf.rb +164 -0
  54. data/tests/test_router_ospf_vrf.rb +753 -0
  55. data/tests/test_snmpcommunity.rb +344 -0
  56. data/tests/test_snmpgroup.rb +71 -0
  57. data/tests/test_snmpserver.rb +443 -0
  58. data/tests/test_snmpuser.rb +803 -0
  59. data/tests/test_tacacs_server.rb +388 -0
  60. data/tests/test_tacacs_server_host.rb +391 -0
  61. data/tests/test_vlan.rb +264 -0
  62. data/tests/test_vtp.rb +319 -0
  63. data/tests/test_yum.rb +106 -0
  64. metadata +188 -0
@@ -0,0 +1,241 @@
1
+ #
2
+ # NXAPI implementation of Interface OSPF class
3
+ #
4
+ # March 2015, Alex Hunsberger
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
+ require File.join(File.dirname(__FILE__), 'node')
21
+ require File.join(File.dirname(__FILE__), 'interface')
22
+ # Interestingly enough, interface OSPF configuration can exist completely
23
+ # independent of router OSPF configuration... so we don't need RouterOspf here.
24
+
25
+ module Cisco
26
+ class InterfaceOspf
27
+ attr_reader :interface, :ospf_name, :area
28
+
29
+ @@node = Node.instance
30
+
31
+ def initialize(int_name, ospf_name, area, create=true)
32
+ raise TypeError unless int_name.is_a? String
33
+ raise TypeError unless ospf_name.is_a? String
34
+ raise TypeError unless area.is_a? String
35
+ raise ArgumentError unless int_name.length > 0
36
+ raise ArgumentError unless ospf_name.length > 0
37
+ raise ArgumentError unless area.length > 0
38
+
39
+ # normalize
40
+ int_name = int_name.downcase
41
+ @interface = Interface.interfaces[int_name]
42
+ raise "interface #{int_name} does not exist" if @interface.nil?
43
+
44
+ @ospf_name = ospf_name
45
+ @area = area
46
+
47
+ if create
48
+ # enable feature ospf if it isn't
49
+ RouterOspf.enable unless RouterOspf.enabled
50
+
51
+ @@node.config_set("interface_ospf", "area", @interface.name,
52
+ "", @ospf_name, area)
53
+ end
54
+ end
55
+
56
+ # can't re-use Interface.interfaces because we need to filter based on
57
+ # "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?
60
+ ints = {}
61
+
62
+ intf_list = @@node.config_get("interface", "all_interfaces")
63
+ return ints if intf_list.nil?
64
+ intf_list.each do |name|
65
+ match = @@node.config_get("interface_ospf", "area", name)
66
+ next if match.nil?
67
+ # should only be a single match under a given interface
68
+ match = match.first
69
+ # ip router ospf <name> area <area>
70
+ ospf = match[0]
71
+ area = match[1]
72
+ next unless ospf_name.nil? or ospf == ospf_name
73
+ int = name.downcase
74
+ ints[int] = InterfaceOspf.new(int, ospf, area, false)
75
+ end
76
+ ints
77
+ end
78
+
79
+ def destroy
80
+ @@node.config_set("interface_ospf", "area", @interface.name,
81
+ "no", @ospf_name, @area)
82
+ # Reset everything else back to default as well:
83
+ self.message_digest = default_message_digest
84
+ message_digest_key_set(default_message_digest_key_id, "", "", "")
85
+ self.cost = default_cost
86
+ self.hello_interval = default_hello_interval
87
+ @@node.config_set("interface_ospf", "dead_interval",
88
+ @interface.name, "no", "")
89
+ self.passive_interface = default_passive_interface if passive_interface
90
+ end
91
+
92
+ def default_message_digest
93
+ @@node.config_get_default("interface_ospf", "message_digest")
94
+ end
95
+
96
+ def message_digest
97
+ not @@node.config_get("interface_ospf", "message_digest",
98
+ @interface.name).nil?
99
+ end
100
+
101
+ # interface %s
102
+ # %s ip ospf authentication message-digest
103
+ def message_digest=(enable)
104
+ @@node.config_set("interface_ospf", "message_digest", @interface.name,
105
+ enable ? "" : "no")
106
+ end
107
+
108
+ def default_message_digest_key_id
109
+ @@node.config_get_default("interface_ospf", "message_digest_key_id")
110
+ end
111
+
112
+ def message_digest_key_id
113
+ match = @@node.config_get("interface_ospf", "message_digest_key_id",
114
+ @interface.name)
115
+ # regex in yaml returns an array result, use .first to get match
116
+ match.nil? ? default_message_digest_key_id : match.first.to_i
117
+ end
118
+
119
+ def default_message_digest_algorithm_type
120
+ @@node.config_get_default("interface_ospf",
121
+ "message_digest_alg_type").to_sym
122
+ end
123
+
124
+ def message_digest_algorithm_type
125
+ match = @@node.config_get("interface_ospf", "message_digest_alg_type",
126
+ @interface.name)
127
+ # regex in yaml returns an array result, use .first to get match
128
+ match.nil? ? default_message_digest_algorithm_type :
129
+ match.first.to_sym
130
+ end
131
+
132
+ def default_message_digest_encryption_type
133
+ Encryption.cli_to_symbol(
134
+ @@node.config_get_default("interface_ospf", "message_digest_enc_type"))
135
+ end
136
+
137
+ def message_digest_encryption_type
138
+ match = @@node.config_get("interface_ospf", "message_digest_enc_type",
139
+ @interface.name)
140
+ # regex in yaml returns an array result, use .first to get match
141
+ match.nil? ? default_message_digest_encryption_type :
142
+ Encryption.cli_to_symbol(match.first)
143
+ end
144
+
145
+ def message_digest_password
146
+ match = @@node.config_get("interface_ospf", "message_digest_password",
147
+ @interface.name)
148
+ match.nil? ? nil : match.first
149
+ end
150
+
151
+ # interface %s
152
+ # %s ip ospf message-digest-key %d %s %d %s
153
+ def message_digest_key_set(keyid, algtype, enctype, enc)
154
+ current_keyid = message_digest_key_id
155
+ if keyid == default_message_digest_key_id && current_keyid != keyid
156
+ @@node.config_set("interface_ospf", "message_digest_key_set",
157
+ @interface.name, "no", current_keyid,
158
+ "", "", "")
159
+ elsif keyid != default_message_digest_key_id
160
+ raise TypeError unless enc.is_a?(String)
161
+ raise ArgumentError unless enc.length > 0
162
+ enctype = Encryption.symbol_to_cli(enctype)
163
+ @@node.config_set("interface_ospf", "message_digest_key_set",
164
+ @interface.name, "", keyid, algtype, enctype, enc)
165
+ end
166
+ end
167
+
168
+ def cost
169
+ match = @@node.config_get("interface_ospf", "cost", @interface.name)
170
+ # regex in yaml returns an array result, use .first to get match
171
+ match.nil? ? default_cost : match.first.to_i
172
+ end
173
+
174
+ def default_cost
175
+ @@node.config_get_default("interface_ospf", "cost")
176
+ end
177
+
178
+ # interface %s
179
+ # ip ospf cost %d
180
+ def cost=(c)
181
+ if c == default_cost
182
+ @@node.config_set("interface_ospf", "cost", @interface.name, "no", "")
183
+ else
184
+ @@node.config_set("interface_ospf", "cost", @interface.name, "", c)
185
+ end
186
+ end
187
+
188
+ def hello_interval
189
+ match = @@node.config_get("interface_ospf", "hello_interval",
190
+ @interface.name)
191
+ # regex in yaml returns an array result, use .first to get match
192
+ match.nil? ? default_hello_interval : match.first.to_i
193
+ end
194
+
195
+ def default_hello_interval
196
+ @@node.config_get_default("interface_ospf", "hello_interval")
197
+ end
198
+
199
+ # interface %s
200
+ # ip ospf hello-interval %d
201
+ def hello_interval=(interval)
202
+ @@node.config_set("interface_ospf", "hello_interval",
203
+ @interface.name, "", interval.to_i)
204
+ end
205
+
206
+ def dead_interval
207
+ match = @@node.config_get("interface_ospf", "dead_interval",
208
+ @interface.name)
209
+ # regex in yaml returns an array result, use .first to get match
210
+ match.nil? ? default_dead_interval : match.first.to_i
211
+ end
212
+
213
+ def default_dead_interval
214
+ @@node.config_get_default("interface_ospf", "dead_interval")
215
+ end
216
+
217
+ # interface %s
218
+ # ip ospf dead-interval %d
219
+ def dead_interval=(interval)
220
+ @@node.config_set("interface_ospf", "dead_interval",
221
+ @interface.name, "", interval.to_i)
222
+ end
223
+
224
+ def default_passive_interface
225
+ @@node.config_get_default("interface_ospf", "passive_interface")
226
+ end
227
+
228
+ def passive_interface
229
+ not @@node.config_get("interface_ospf", "passive_interface",
230
+ @interface.name).nil?
231
+ end
232
+
233
+ # interface %s
234
+ # %s ip ospf passive-interface
235
+ def passive_interface=(enable)
236
+ raise TypeError unless enable == true or enable == false
237
+ @@node.config_set("interface_ospf", "passive_interface", @interface.name,
238
+ enable ? "" : "no")
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,673 @@
1
+ # Cisco node helper class. Abstracts away the details of the underlying
2
+ # transport (whether NXAPI or some other future transport) and provides
3
+ # various convenient helper methods.
4
+ #
5
+ # December 2014, Glenn F. Matthews
6
+ #
7
+ # Copyright (c) 2014-2015 Cisco and/or its affiliates.
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+
21
+ require 'singleton'
22
+
23
+ require 'cisco_nxapi'
24
+ require File.join(File.dirname(__FILE__), 'command_reference')
25
+
26
+ module Cisco
27
+ # Error class raised by the config_set and config_get APIs if the
28
+ # device encounters an issue trying to act on the requested CLI.
29
+ #
30
+ # command - the specific CLI that was rejected
31
+ # clierror - any error string from the device
32
+ class CliError < RuntimeError
33
+ attr_reader :command, :clierror, :previous
34
+ def initialize(command, clierror, previous)
35
+ @command = command
36
+ @clierror = clierror
37
+ @previous = previous
38
+ end
39
+
40
+ def message
41
+ "CliError: '#{@command}' rejected with message:\n'#{@clierror}'"
42
+ end
43
+ end
44
+
45
+ # class Cisco::Node
46
+ # Singleton representing the network node (switch/router) that is
47
+ # running this code. The singleton is lazily instantiated, meaning that
48
+ # it doesn't exist until some client requests it (with Node.instance())
49
+
50
+ class Node
51
+ include Singleton
52
+
53
+ # BEGIN NODE API
54
+ # This is most of what a client/provider should need to code against.
55
+ # Actual implementations of these methods are later in this file.
56
+
57
+ # Convenience wrapper for show(command, :structured).
58
+ # Uses CommandReference to look up the given show command and key
59
+ # of interest, executes that command, and returns the value corresponding
60
+ # to that key.
61
+ #
62
+ # @raise [IndexError] if the given (feature, name) pair is not in the
63
+ # CommandReference data or if the data doesn't have values defined
64
+ # for the 'config_get' and 'config_get_token' fields.
65
+ # @raise [Cisco::CliError] if the given command is rejected by the device.
66
+ #
67
+ # @param feature [String]
68
+ # @param name [String]
69
+ # @return [String]
70
+ # @example config_get("show_version", "system_image")
71
+ def config_get(feature, name)
72
+ end
73
+
74
+ # Uses CommandReference to lookup the default value for a given
75
+ # feature and feature property.
76
+ #
77
+ # @raise [IndexError] if the given (feature, name) pair is not in the
78
+ # CommandReference data or if the data doesn't have values defined
79
+ # for the 'default_value' field.
80
+ # @param feature [String]
81
+ # @param name [String]
82
+ # @return [String]
83
+ # @example config_get_default("vtp", "file")
84
+ def config_get_default(feature, name)
85
+ end
86
+
87
+ # Uses CommandReference to look up the given config command(s) of interest
88
+ # and then applies the configuration.
89
+ #
90
+ # @raise [IndexError] if no relevant cmd_ref config_set exists
91
+ # @raise [ArgumentError] if too many or too few args are provided.
92
+ # @raise [Cisco::CliError] if any command is rejected by the device.
93
+ #
94
+ # @param feature [String]
95
+ # @param name [String]
96
+ # @param args [*String] zero or more args to be substituted into the cmdref.
97
+ # @example config_set("vtp", "domain", "example.com")
98
+ def config_set(feature, name, *args)
99
+ end
100
+
101
+ # Clear the cache of CLI output results.
102
+ #
103
+ # If cache_auto is true (default) then this will be performed automatically
104
+ # whenever a config_set() is called, but providers may also call this
105
+ # to explicitly force the cache to be cleared.
106
+ def cache_flush
107
+ end
108
+
109
+ # END NODE API
110
+ # Here and below are implementation details and private APIs that most
111
+ # providers shouldn't need to know about or use.
112
+
113
+ attr_reader :cmd_ref, :client
114
+
115
+ # For unit testing - we won't know the node connection info at load time.
116
+ @@lazy_connect = false
117
+
118
+ def Node.lazy_connect=(val)
119
+ @@lazy_connect = val
120
+ end
121
+
122
+ def initialize
123
+ @client = nil
124
+ @cmd_ref = nil
125
+ connect unless @@lazy_connect
126
+ end
127
+
128
+ def to_s
129
+ @client.to_s
130
+ end
131
+
132
+ # "hidden" API - used for UT but shouldn't be used elsewhere
133
+ def connect(*args)
134
+ @client = CiscoNxapi::NxapiClient.new(*args)
135
+ @cmd_ref = CommandReference::CommandReference.new(product_id)
136
+ cache_flush
137
+ end
138
+
139
+ # TODO: remove me
140
+ def reload
141
+ @client.reload
142
+ end
143
+
144
+ # hidden as well
145
+ attr_reader :client
146
+
147
+ def cache_flush
148
+ @client.cache_flush
149
+ end
150
+
151
+ def cache_enable?
152
+ @client.cache_enable?
153
+ end
154
+
155
+ def cache_enable=(enable)
156
+ @client.cache_enable = enable
157
+ end
158
+
159
+ def cache_auto?
160
+ @client.cache_auto?
161
+ end
162
+
163
+ def cache_auto=(enable)
164
+ @client.cache_auto = enable
165
+ end
166
+
167
+ # Helper method for converting token strings to regexps. This helper
168
+ # facilitates non-standard regexp options like ignore-case.
169
+ # Example inputs:
170
+ # token = ["/%s/i", "/%s foo %s/", "/zzz/i"]
171
+ # args = ["LoopBack2", "no", "bar"]
172
+ # Expected outputs:
173
+ # [/LoopBack2/i, /no foo bar/, /zzz/i]
174
+ #
175
+ def token_str_to_regexp(token, args)
176
+ unless args[0].is_a? Hash
177
+ expected_args = token.join.scan(/%/).length
178
+ raise "Given #{args.length} args, but token #{token} requires " +
179
+ "#{expected_args}" unless args.length == expected_args
180
+ end
181
+ # replace all %s with *args
182
+ token.map! { |str| sprintf(str, *args.shift(str.scan(/%/).length)) }
183
+ # convert all to Regexp objects
184
+ token.map! { |str|
185
+ if str[-2..-1] == '/i'
186
+ Regexp.new(str[1..-3], Regexp::IGNORECASE)
187
+ else
188
+ Regexp.new(str[1..-2])
189
+ end
190
+ }
191
+ token
192
+ end
193
+
194
+ # Helper method to replace <> place holders in the config_get_token
195
+ # and config_get_token_append yaml entries.
196
+ #
197
+ # @param regexp [String][Array] regexp entry with <> placeholders
198
+ # @param values [Hash] Hash of named values to replace each <>
199
+ # @return [String]
200
+ def replace_token_ids(regexp, values)
201
+ final = replace_token_ids_string(regexp, values) if regexp.is_a?(String)
202
+ final = replace_token_ids_array(regexp, values) if regexp.is_a?(Array)
203
+ final
204
+ end
205
+
206
+ # @param regexp [String] regexp entry with <> placeholders
207
+ # @param values [Hash] Hash of named values to replace each <>
208
+ # @return [String]
209
+ def replace_token_ids_string(regexp, values)
210
+ replace = regexp.scan(/<(\S+)>/).flatten.map(&:to_sym)
211
+ replace.each do |item|
212
+ regexp = regexp.sub "<#{item}>",
213
+ values[item].to_s if values.key?(item)
214
+ end
215
+ # Only return lines that actually replaced ids or did not have any
216
+ # ids to replace. Implicit nil returned if not.
217
+ return regexp if /<\S+>/.match(regexp).nil?
218
+ end
219
+
220
+ # @param regexp [Array] regexp entry with <> placeholders
221
+ # @param values [Hash] Hash of named values to replace each <>
222
+ # @return [String]
223
+ def replace_token_ids_array(regexp, values)
224
+ final_regexp = []
225
+ regexp.each do |line|
226
+ final_regexp.push(replace_token_ids_string(line, values))
227
+ end
228
+ final_regexp
229
+ end
230
+
231
+ # Helper method to build a multi-line config_get_token if
232
+ # the feature, name contains a config_get_token_append entry.
233
+ #
234
+ # @param feature [String]
235
+ # @param ref [CommandReference::CmdRef]
236
+ # @return [String, Array]
237
+ def build_config_get_token(feature, ref, args)
238
+ raise "lazy_connect specified but did not request connect" unless @cmd_ref
239
+ # Why clone token? A bug in some ruby versions caused token to convert
240
+ # to type Regexp unexpectedly. The clone hard copy resolved it.
241
+
242
+ # If the options are presented as type Hash process as
243
+ # key-value replacement pairs
244
+ return ref.config_get_token.clone unless args[0].is_a?(Hash)
245
+ options = args[0]
246
+ token = []
247
+ # Use _template yaml entry if config_get_token_append
248
+ if ref.to_s[/config_get_token_append/]
249
+ # Get yaml feature template:
250
+ template = @cmd_ref.lookup(feature, "_template")
251
+ # Process config_get_token: from template:
252
+ token.push(replace_token_ids(template.config_get_token, options))
253
+ # Process config_get_token_append sequence: from template:
254
+ template.config_get_token_append.each do |line|
255
+ token.push(replace_token_ids(line, options))
256
+ end
257
+ # Add feature->property config_get_token append line
258
+ token.push(ref.config_get_token_append)
259
+ else
260
+ token.push(replace_token_ids(ref.config_get_token, options))
261
+ end
262
+ token.flatten!
263
+ token.compact!
264
+ token
265
+ end
266
+
267
+ # Helper method to use the feature, name config_get
268
+ # if present else use feature, "template" config_get
269
+ #
270
+ # @param feature [String]
271
+ # @param ref [CommandReference::CmdRef]
272
+ # @param type [Symbol]
273
+ # @return [String, Array]
274
+ def build_config_get(feature, ref, type)
275
+ raise "lazy_connect specified but did not request connect" unless @cmd_ref
276
+ # Use feature name config_get string if present
277
+ # else use feature template: config_get
278
+ if ref.hash.key?("config_get")
279
+ return show(ref.config_get, type)
280
+ else
281
+ template = @cmd_ref.lookup(feature, "_template")
282
+ return show(template.config_get, type)
283
+ end
284
+ end
285
+
286
+ # Helper method to build a multi-line config_set if
287
+ # the feature, name contains a config_get_set_append
288
+ # yaml entry.
289
+ #
290
+ # @param feature [String]
291
+ # @param ref [CommandReference::CmdRef]
292
+ # @return [String, Array]
293
+ def build_config_set(feature, ref, args)
294
+ raise "lazy_connect specified but did not request connect" unless @cmd_ref
295
+ # If the options are presented as type Hash process as
296
+ # key-value replacement pairs
297
+ return ref.config_set unless args[0].is_a?(Hash)
298
+ options = args[0]
299
+ config_set = []
300
+ # Use _template yaml entry if config_set_append
301
+ if ref.to_s[/config_set_append/]
302
+ # Get yaml feature template:
303
+ template = @cmd_ref.lookup(feature, "_template")
304
+ # Process config_set: from template:
305
+ config_set.push(replace_token_ids(template.config_set, options))
306
+ # Process config_set_append sequence: from template:
307
+ template.config_set_append.each do |line|
308
+ config_set.push(replace_token_ids(line, options))
309
+ end
310
+ # Add feature->property config_set append line
311
+ config_set.push(replace_token_ids(ref.config_set_append, options))
312
+ else
313
+ config_set.push(replace_token_ids(ref.config_set, options))
314
+ end
315
+ config_set.flatten!
316
+ config_set.compact!
317
+ config_set
318
+ end
319
+
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}")
443
+ end
444
+ 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
460
+ end
461
+
462
+ # Send a config command to the device.
463
+ # In general, clients should use config_set() rather than calling
464
+ # this function directly.
465
+ #
466
+ # @raise [Cisco::CliError] if any command is rejected by the device.
467
+ def config(commands)
468
+ @client.config(commands)
469
+ rescue CiscoNxapi::CliError => e
470
+ raise Cisco::CliError.new(e.input, e.clierror, e.previous)
471
+ end
472
+
473
+ # Send a show command to the device.
474
+ # In general, clients should use config_get() rather than calling
475
+ # this function directly.
476
+ #
477
+ # @raise [Cisco::CliError] if any command is rejected by the device.
478
+ def show(command, type=:ascii)
479
+ @client.show(command, type)
480
+ rescue CiscoNxapi::CliError => e
481
+ raise Cisco::CliError.new(e.input, e.clierror, e.previous)
482
+ end
483
+
484
+ # @return [String] such as "Cisco Nexus Operating System (NX-OS) Software"
485
+ 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]
489
+ end
490
+
491
+ # @return [String] such as "6.0(2)U5(1) [build 6.0(2)U5(0.941)]"
492
+ def os_version
493
+ config_get("show_version", "version")
494
+ end
495
+
496
+ # @return [String] such as "Nexus 3048 Chassis"
497
+ def product_description
498
+ config_get("show_version", "description")
499
+ end
500
+
501
+ # @return [String] such as "N3K-C3048TP-1GE"
502
+ def product_id
503
+ if @cmd_ref
504
+ return config_get("inventory", "productid")
505
+ else
506
+ # We use this function to *find* the appropriate CommandReference
507
+ entries = show("show inventory", :structured)
508
+ return entries["TABLE_inv"]["ROW_inv"][0]["productid"]
509
+ end
510
+ end
511
+
512
+ # @return [String] such as "V01"
513
+ def product_version_id
514
+ config_get("inventory", "versionid")
515
+ end
516
+
517
+ # @return [String] such as "FOC1722R0ET"
518
+ def product_serial_number
519
+ config_get("inventory", "serialnum")
520
+ end
521
+
522
+ # @return [String] such as "bxb-oa-n3k-7"
523
+ def host_name
524
+ config_get("show_version", "host_name")
525
+ end
526
+
527
+ # @return [String] such as "example.com"
528
+ def domain_name
529
+ result = config_get("domain_name", "domain_name")
530
+ if result.nil?
531
+ return ""
532
+ else
533
+ return result[0]
534
+ end
535
+ end
536
+
537
+ # @return [Integer] System uptime, in seconds
538
+ def system_uptime
539
+ cache_flush
540
+ t = config_get("show_system", "uptime")
541
+ raise "failed to retrieve system uptime" if t.nil?
542
+ t = t.shift
543
+ # time units: t = ["0", "23", "15", "49"]
544
+ t.map!(&:to_i)
545
+ d, h, m, s = t
546
+ (s + 60 * (m + 60 * (h + 24 * (d))))
547
+ end
548
+
549
+ # @return [String] timestamp of last reset time
550
+ def last_reset_time
551
+ output = config_get("show_version", "last_reset_time")
552
+ return "" if output.nil?
553
+ # NX-OS may provide leading/trailing whitespace:
554
+ # " Sat Oct 25 00:39:25 2014\n"
555
+ # so be sure to strip() it down to the actual string.
556
+ output.strip
557
+ end
558
+
559
+ # @return [String] such as "Reset Requested by CLI command reload"
560
+ def last_reset_reason
561
+ config_get("show_version", "last_reset_reason")
562
+ end
563
+
564
+ # @return [Float] combined user/kernel CPU utilization
565
+ 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
569
+ end
570
+
571
+ # @return [String] such as
572
+ # "bootflash:///n3000-uk9-kickstart.6.0.2.U5.0.941.bin"
573
+ def boot
574
+ config_get("show_version", "boot_image")
575
+ end
576
+
577
+ # @return [String] such as
578
+ # "bootflash:///n3000-uk9.6.0.2.U5.0.941.bin"
579
+ def system
580
+ config_get("show_version", "system_image")
581
+ end
582
+ end
583
+
584
+ # Convenience wrapper for find_ascii. Operates under the assumption
585
+ # that there will be zero or one matches for the given query
586
+ # and returns the match string (or "") rather than an array.
587
+ #
588
+ # @raise [RuntimeError] if more than one match is found.
589
+ #
590
+ # @param body [String] The body of text to search
591
+ # @param regex_query [Regex] The regular expression to match
592
+ # @param parents [*Regex] zero or more regular expressions defining
593
+ # the parent configs to filter by.
594
+ # @return [String] the matching (sub)string or "" if no match.
595
+ #
596
+ # @example Get the domain name if any
597
+ # domain_name = find_one_ascii(running_cfg, "ip domain-name (.*)")
598
+ # => 'example.com'
599
+ def find_one_ascii(body, regex_query, *parent_cfg)
600
+ matches = find_ascii(body, regex_query, *parent_cfg)
601
+ return "" if matches.nil?
602
+ raise RuntimeError if matches.length > 1
603
+ matches[0]
604
+ end
605
+ module_function :find_one_ascii
606
+
607
+ # Method for working with hierarchical show command output such as
608
+ # "show running-config". Searches the given multi-line string
609
+ # for all matches to the given regex_query. If parents is provided,
610
+ # the matches will be filtered to only those that are located "under"
611
+ # the given parent sequence (as determined by indentation).
612
+ #
613
+ # @param body [String] The body of text to search
614
+ # @param regex_query [Regex] The regular expression to match
615
+ # @param parents [*Regex] zero or more regular expressions defining
616
+ # the parent configs to filter by.
617
+ # @return [[String], nil] array of matching (sub)strings, else nil.
618
+ #
619
+ # @example Find all OSPF router names in the running-config
620
+ # ospf_names = find_ascii(running_cfg, /^router ospf (\d+)/)
621
+ #
622
+ # @example Find all address-family types under the given BGP router
623
+ # bgp_afs = find_ascii(show_run_bgp, /^address-family (.*)/,
624
+ # /^router bgp #{ASN}/)
625
+ def find_ascii(body, regex_query, *parent_cfg)
626
+ return nil if body.nil? or regex_query.nil?
627
+
628
+ # get subconfig
629
+ parent_cfg.each { |p| body = find_subconfig(body, p) }
630
+ if body.nil?
631
+ return nil
632
+ else
633
+ # find matches and return as array of String if it only does one
634
+ # match in the regex. Otherwise return array of array
635
+ match = body.split("\n").map { |s| s.scan(regex_query) }
636
+ match = match.flatten(1)
637
+ return nil if match.empty?
638
+ match = match.flatten if match[0].is_a?(Array) and match[0].length == 1
639
+ return match
640
+ end
641
+ end
642
+ module_function :find_ascii
643
+
644
+ # Returns the subsection associated with the given
645
+ # line of config
646
+ # @param [String] the body of text to search
647
+ # @param [Regex] the regex key of the config for which
648
+ # to retrieve the subsection
649
+ # @return [String, nil] the subsection of body, de-indented
650
+ # appropriately, or nil if no such subsection exists.
651
+ def find_subconfig(body, regex_query)
652
+ return nil if body.nil? or regex_query.nil?
653
+
654
+ rows = body.split("\n")
655
+ match_row_index = rows.index { |row| regex_query =~ row }
656
+ return nil if match_row_index.nil?
657
+
658
+ cur = match_row_index+1
659
+ subconfig = []
660
+
661
+ until (/\A\s+.*/ =~ rows[cur]).nil? or cur == rows.length
662
+ subconfig << rows[cur]
663
+ cur += 1
664
+ end
665
+ return nil if subconfig.empty?
666
+ # Strip an appropriate minimal amount of leading whitespace from
667
+ # all lines in the subconfig
668
+ min_leading = subconfig.map { |line| line[/\A */].size }.min
669
+ subconfig = subconfig.map { |line| line[min_leading..-1] }
670
+ subconfig.join("\n")
671
+ end
672
+ module_function :find_subconfig
673
+ end