cisco_node_utils 0.9.0

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