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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +3 -0
- data/.rubocop_todo.yml +293 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +31 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +113 -0
- data/Rakefile +4 -0
- data/cisco_node_utils.gemspec +30 -0
- data/lib/cisco_node_utils.rb +33 -0
- data/lib/cisco_node_utils/README_YAML.md +333 -0
- data/lib/cisco_node_utils/cisco_cmn_utils.rb +92 -0
- data/lib/cisco_node_utils/command_reference.rb +415 -0
- data/lib/cisco_node_utils/command_reference_common.yaml +845 -0
- data/lib/cisco_node_utils/command_reference_n3064.yaml +13 -0
- data/lib/cisco_node_utils/command_reference_n7k.yaml +48 -0
- data/lib/cisco_node_utils/command_reference_n9k.yaml +35 -0
- data/lib/cisco_node_utils/configparser_lib.rb +196 -0
- data/lib/cisco_node_utils/interface.rb +501 -0
- data/lib/cisco_node_utils/interface_ospf.rb +241 -0
- data/lib/cisco_node_utils/node.rb +673 -0
- data/lib/cisco_node_utils/platform.rb +184 -0
- data/lib/cisco_node_utils/platform_info.rb +58 -0
- data/lib/cisco_node_utils/platform_info.yaml +10 -0
- data/lib/cisco_node_utils/router_ospf.rb +96 -0
- data/lib/cisco_node_utils/router_ospf_vrf.rb +258 -0
- data/lib/cisco_node_utils/snmpcommunity.rb +91 -0
- data/lib/cisco_node_utils/snmpgroup.rb +55 -0
- data/lib/cisco_node_utils/snmpserver.rb +150 -0
- data/lib/cisco_node_utils/snmpuser.rb +342 -0
- data/lib/cisco_node_utils/tacacs_server.rb +175 -0
- data/lib/cisco_node_utils/tacacs_server_host.rb +128 -0
- data/lib/cisco_node_utils/version.rb +17 -0
- data/lib/cisco_node_utils/vlan.rb +153 -0
- data/lib/cisco_node_utils/vtp.rb +127 -0
- data/lib/cisco_node_utils/yum.rb +84 -0
- data/tests/basetest.rb +93 -0
- data/tests/ciscotest.rb +136 -0
- data/tests/cmd_config.yaml +51 -0
- data/tests/cmd_config_invalid.yaml +16 -0
- data/tests/test_all_cisco.rb +46 -0
- data/tests/test_command_config.rb +192 -0
- data/tests/test_command_reference.rb +222 -0
- data/tests/test_interface.rb +1017 -0
- data/tests/test_interface_ospf.rb +763 -0
- data/tests/test_interface_svi.rb +267 -0
- data/tests/test_interface_switchport.rb +722 -0
- data/tests/test_node.rb +108 -0
- data/tests/test_node_ext.rb +450 -0
- data/tests/test_platform.rb +188 -0
- data/tests/test_router_ospf.rb +164 -0
- data/tests/test_router_ospf_vrf.rb +753 -0
- data/tests/test_snmpcommunity.rb +344 -0
- data/tests/test_snmpgroup.rb +71 -0
- data/tests/test_snmpserver.rb +443 -0
- data/tests/test_snmpuser.rb +803 -0
- data/tests/test_tacacs_server.rb +388 -0
- data/tests/test_tacacs_server_host.rb +391 -0
- data/tests/test_vlan.rb +264 -0
- data/tests/test_vtp.rb +319 -0
- data/tests/test_yum.rb +106 -0
- 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
|