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,92 @@
1
+ # Common Utilities for Puppet Resources.
2
+ #
3
+ # Copyright (c) 2014-2015 Cisco and/or its affiliates.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Cisco
18
+ # global constants
19
+ DEFAULT_INSTANCE_NAME = 'default'
20
+
21
+ class Encryption
22
+ # password encryption types
23
+ def Encryption.cli_to_symbol(cli)
24
+ case cli
25
+ when "0", 0
26
+ :cleartext
27
+ when "3", 3
28
+ :"3des" # yuck :-(
29
+ when "5", 5
30
+ :md5
31
+ when "6", 6
32
+ :aes
33
+ when "7", 7
34
+ :cisco_type_7
35
+ else
36
+ raise KeyError
37
+ end
38
+ end
39
+
40
+ def Encryption.symbol_to_cli(symbol)
41
+ symbol = symbol.downcase if symbol.is_a? String
42
+ case symbol
43
+ when :cleartext, :none, "cleartext", "none", "0", 0
44
+ "0"
45
+ when :"3des", "3des", "3", 3
46
+ "3"
47
+ when :md5, "md5", "5", 5
48
+ "5"
49
+ when :aes, "aes", "6", 6
50
+ "6"
51
+ when :cisco_type_7, :type_7, "cisco_type_7", "type_7", "7", 7
52
+ "7"
53
+ else
54
+ raise KeyError
55
+ end
56
+ end
57
+ end
58
+
59
+ class ChefUtils
60
+ def ChefUtils.generic_prop_set(klass, rlbname, props)
61
+ props.each do |prop|
62
+ klass.instance_eval {
63
+ # Helper Chef setter method, e.g.:
64
+ # if @new_resource.foo.nil?
65
+ # def_prop = @rlb.default_foo
66
+ # @new_resource.foo(def_prop)
67
+ # end
68
+ # current = @rlb.foo
69
+ # if current != @new_resource.foo
70
+ # converge_by("update foo '#{current}' => " +
71
+ # "'#{@new_resource.foo}'") do
72
+ # @rlb.foo=(@new_resource.foo)
73
+ # end
74
+ # end
75
+ if @new_resource.send(prop).nil?
76
+ def_prop = instance_variable_get(rlbname).send("default_#{prop}")
77
+ # Set resource to default if recipe property is not specified
78
+ @new_resource.send(prop, def_prop)
79
+ end
80
+ current = instance_variable_get(rlbname).send(prop)
81
+ if current != @new_resource.send(prop)
82
+ converge_by("update #{prop} '#{current}' => " +
83
+ "'#{@new_resource.send(prop)}'") do
84
+ instance_variable_get(rlbname).send("#{prop}=",
85
+ @new_resource.send(prop))
86
+ end
87
+ end
88
+ }
89
+ end
90
+ end
91
+ end # class ChefUtils
92
+ end # module Cisco
@@ -0,0 +1,415 @@
1
+ # CommandReference module for testing.
2
+ #
3
+ # Copyright (c) 2014-2015 Cisco and/or its affiliates.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'yaml'
18
+
19
+ module CommandReference
20
+ # Helper class to match product id with reference files.
21
+ class CommandPlatformFile
22
+ attr_reader :regex, :file
23
+
24
+ def initialize(match_expression, reference_file)
25
+ self.regex = match_expression
26
+ self.file = reference_file
27
+ end
28
+
29
+ def regex=(expression)
30
+ if expression.is_a? Regexp
31
+ @regex = expression
32
+ else
33
+ raise ArgumentError
34
+ end
35
+ end
36
+
37
+ def file=(file)
38
+ if file.is_a? String
39
+ @file = file
40
+ else
41
+ raise ArgumentError
42
+ end
43
+ end
44
+
45
+ def match(product)
46
+ @regex.match(product)
47
+ end
48
+ end
49
+
50
+ # Control a reference for an attribute.
51
+ class CmdRef
52
+ attr_reader :feature
53
+ attr_reader :name
54
+ attr_reader :sources
55
+ attr_reader :hash
56
+
57
+ @@KEYS = %w(default_value
58
+ config_set config_set_append
59
+ config_get config_get_token config_get_token_append
60
+ test_config_get test_config_get_regex test_config_result)
61
+
62
+ def initialize(feature, name, ref, source)
63
+ raise ArgumentError, "'#{ref}' is not a hash." unless ref.is_a? Hash
64
+
65
+ @feature = feature
66
+ @name = name
67
+ @hash = {}
68
+
69
+ @sources = []
70
+ merge(ref, source)
71
+ end
72
+
73
+ # Overwrite values from more specific references.
74
+ def merge(values, file)
75
+ values.each { |key, value|
76
+ unless @@KEYS.include?(key)
77
+ raise "Unrecognized key #{key} for #{feature}, #{name} in #{file}"
78
+ end
79
+ if value.nil?
80
+ # Some attributes can store an explicit nil.
81
+ # Others treat this as unset (allowing a platform to override common).
82
+ if key == "default_value"
83
+ @hash[key] = value
84
+ else
85
+ @hash.delete(key)
86
+ end
87
+ else
88
+ @hash[key] = value
89
+ end
90
+ }
91
+ @sources << file
92
+ end
93
+
94
+ def convert_to_constant(value)
95
+ # NOTE: This method is now deprecated and should not be used for future
96
+ # development.
97
+ #
98
+ # If value is a string and it is empty OR the first letter is lower case
99
+ # then leave value untouched.
100
+ # If value is a string and the first letter is uppercase this indicates
101
+ # that it could be a constant in Ruby so attempt to convert it to a Constant.
102
+ if value.is_a?(String) and not value.empty?
103
+ if value[0].chr == value[0].chr.upcase
104
+ begin
105
+ value = Object.const_get(value) if Object.const_defined?(value)
106
+ rescue NameError
107
+ # debug("#{value} looks like a constant but is not")
108
+ end
109
+ end
110
+ end
111
+ value
112
+ end
113
+
114
+ def test_config_result(value)
115
+ result = @hash["test_config_result"][value]
116
+ convert_to_constant(result)
117
+ end
118
+
119
+ def method_missing(method_name, *args, &block)
120
+ super(method_name, *args, &block) unless @@KEYS.include?(method_name.to_s)
121
+ method_name = method_name.to_s
122
+ unless @hash.include?(method_name)
123
+ raise IndexError, "No #{method_name} defined for #{@feature}, #{@name}"
124
+ end
125
+ # puts("get #{method_name}: '#{@hash[method_name]}'")
126
+ @hash[method_name]
127
+ end
128
+
129
+ # Print useful debugging information about the object.
130
+ def to_s
131
+ str = ""
132
+ str << "Command: #{@feature} #{@name}\n"
133
+ @hash.each { |key, value|
134
+ str << " #{key}: #{value}\n"
135
+ }
136
+ str
137
+ end
138
+
139
+ # Check that all necessary values have been populated.
140
+ def valid?
141
+ return false unless @feature and @name
142
+ true
143
+ end
144
+ end
145
+
146
+ # Builds reference hash for the platform specified in the product id.
147
+ class CommandReference
148
+ @@debug = false
149
+
150
+ def self.debug
151
+ @@debug
152
+ end
153
+
154
+ def self.debug=(value)
155
+ raise ArgumentError, "Debug must be boolean" unless value == true or value == false
156
+ @@debug = value
157
+ end
158
+
159
+ attr_reader :files, :product_id
160
+
161
+ # Constructor.
162
+ # Normal usage is to pass product_id only, in which case all usual YAML
163
+ # files will be located then the list will be filtered down to only those
164
+ # matching the given product_id.
165
+ # For testing purposes (only!) you can pass an explicit list of files to
166
+ # load instead. This list will NOT be filtered further by product_id.
167
+ def initialize(product_id, files=nil)
168
+ @product_id = product_id
169
+ @hash = {}
170
+ if files
171
+ @files = files
172
+ else
173
+ @files = []
174
+ # Hashes are unordered in Ruby 1.8.7. Instead, we use an array of objects.
175
+ platforms = [
176
+ CommandPlatformFile.new(//,
177
+ File.join(File.dirname(__FILE__),
178
+ "command_reference_common.yaml")),
179
+ CommandPlatformFile.new(/N9K/,
180
+ File.join(File.dirname(__FILE__),
181
+ "command_reference_n9k.yaml")),
182
+ CommandPlatformFile.new(/N7K/,
183
+ File.join(File.dirname(__FILE__),
184
+ "command_reference_n7k.yaml")),
185
+ CommandPlatformFile.new(/C3064/,
186
+ File.join(File.dirname(__FILE__),
187
+ "command_reference_n3064.yaml")),
188
+ ]
189
+ # Build array
190
+ platforms.each { |reference|
191
+ @files << reference.file if reference.match(@product_id)
192
+ }
193
+ end
194
+
195
+ build_cmd_ref
196
+ end
197
+
198
+ # Build complete reference hash.
199
+ def build_cmd_ref
200
+ # Example id's: N3K-C3048TP-1GE, N3K-C3064PQ-10GE, N7K-C7009, N7K-C7009
201
+
202
+ debug "Product: #{@product_id}"
203
+ debug "Files being used: #{@files.join(', ')}"
204
+
205
+ reference_yaml = {}
206
+
207
+ @files.each { |file|
208
+ debug "Processing file '#{file}'"
209
+ reference_yaml = load_yaml(file)
210
+
211
+ reference_yaml.each { |feature, names|
212
+ if names.nil? or names.empty?
213
+ raise "No names under feature #{feature}: #{names}"
214
+ elsif @hash[feature].nil?
215
+ @hash[feature] = {}
216
+ else
217
+ debug " Merging feature '#{feature}' retrieved from '#{file}'."
218
+ end
219
+ names.each { |name, values|
220
+ debug " Processing feature '#{feature}' name '#{name}'"
221
+ if @hash[feature][name].nil?
222
+ begin
223
+ @hash[feature][name] = CmdRef.new(feature, name, values, file)
224
+ rescue ArgumentError => e
225
+ raise "Invalid data for '#{feature}', '#{name}': #{e}"
226
+ end
227
+ else
228
+ debug " Merging feature '#{feature}' name '#{name}' from '#{file}'."
229
+ @hash[feature][name].merge(values, file)
230
+ end
231
+ }
232
+ }
233
+ }
234
+
235
+ raise "Missing values in CommandReference." unless valid?
236
+ end
237
+
238
+ # Get the command reference
239
+ def lookup(feature, name)
240
+ begin
241
+ value = @hash[feature][name]
242
+ rescue NoMethodError
243
+ # happens if @hash[feature] doesn't exist
244
+ value = nil
245
+ end
246
+ if value.nil?
247
+ raise IndexError, "No CmdRef defined for #{feature}, #{name}"
248
+ end
249
+ value
250
+ end
251
+
252
+ def empty?
253
+ @hash.empty?
254
+ end
255
+
256
+ # Print debug statements
257
+ def debug(text)
258
+ puts "DEBUG: #{text}" if @@debug
259
+ end
260
+
261
+ def is_mapping?(node)
262
+ # Need to handle both Syck::Map and Psych::Nodes::Mapping
263
+ node.class.ancestors.any? { |name| /Map/ =~ name.to_s }
264
+ end
265
+ private :is_mapping?
266
+
267
+ def get_keys_values_from_map(node)
268
+ if node.class.ancestors.any? { |name| /Psych/ =~ name.to_s }
269
+ # A Psych::Node::Mapping instance has an Array of children in
270
+ # the format [key1, val1, key2, val2]
271
+ key_children = node.children.select.each_with_index { |n, i| i.even? }
272
+ val_children = node.children.select.each_with_index { |n, i| i.odd? }
273
+ else
274
+ # Syck::Map nodes have a .children method but it doesn't work :-(
275
+ # Instead we access the node.value which is a hash.
276
+ key_children = node.value.keys
277
+ val_children = node.value.values
278
+ end
279
+ debug "children of #{node} mapping: #{key_children}, #{val_children}"
280
+ [key_children, val_children]
281
+ end
282
+ private :get_keys_values_from_map
283
+
284
+ # Validate the YAML node tree before converting it into Ruby
285
+ # data structures.
286
+ #
287
+ # @raise RuntimeError if the node tree is not valid by our constraints.
288
+ #
289
+ # @param node Node to be validated, then recurse to its children.
290
+ # @param filename File that YAML was parsed from, for messages
291
+ # @param depth Depth into the node tree
292
+ # @param parents String describing parents of this node, for messages
293
+ def validate_yaml(node, filename, depth=0, parents=nil)
294
+ return unless node and (is_mapping?(node) or node.children)
295
+ # Psych wraps everything in a Document instance, while
296
+ # Syck does not. To keep the "depth" counting consistent,
297
+ # we need to ignore Documents.
298
+ depth += 1 unless node.class.ancestors.any? { |name| /Document/ =~ name.to_s }
299
+ debug "Validating #{node.class} at depth #{depth}"
300
+
301
+ # No special validation for non-mapping nodes - just recurse
302
+ unless is_mapping?(node)
303
+ node.children.each { |child|
304
+ validate_yaml(child, filename, depth, parents)
305
+ }
306
+ return
307
+ end
308
+
309
+ # For Mappings, we validate more extensively:
310
+ # 1. no duplicate keys are allowed (Syck/Psych don't catch this)
311
+ # 2. Features must be listed in alphabetical order for maintainability
312
+
313
+ # Take advantage of our known YAML structure to assign labels by depth
314
+ label = %w(feature name param).fetch(depth, 'key')
315
+
316
+ # Get the key nodes and value nodes under this mapping
317
+ key_children, val_children = get_keys_values_from_map(node)
318
+ # Get an array of key names
319
+ key_arr = key_children.map(&:value)
320
+
321
+ # Make sure no duplicate key names.
322
+ # If searching from the start of the array finds a name at one index,
323
+ # but searching from the end of the array finds it at a different one,
324
+ # then we have a duplicate.
325
+ dup = key_arr.detect { |e| key_arr.index(e) != key_arr.rindex(e) }
326
+ if dup
327
+ msg = "Duplicate #{label} '#{dup}'#{parents} in #{filename}!"
328
+ raise msg
329
+ end
330
+
331
+ =begin
332
+ # Syck does not necessarily preserve ordering of keys in a mapping even during
333
+ # the initial parsing stage. To avoid spurious failures, this is disabled
334
+ # for now. Fixing this may require restructuring our YAML...
335
+ # Enforce alphabetical ordering of features (only).
336
+ # We can extend this later to enforce ordering of names if desired
337
+ # by checking at depth 2 as well.
338
+ if depth == 1
339
+ last_key = nil
340
+ key_arr.each do |key|
341
+ if last_key and key < last_key
342
+ raise RuntimeError, "features out of order (#{last_key} > #{key})"
343
+ end
344
+ last_key = key
345
+ end
346
+ end
347
+ =end
348
+
349
+ # Recurse to the children. We get a little fancy here so as to be able
350
+ # to provide more meaningful debug/error messages, such as:
351
+ # Duplicate param 'default_value' under feature 'foo', name 'bar'
352
+ key_children.zip(val_children).each do |key_node, val_node|
353
+ if parents
354
+ new_parents = parents + ", #{label} '#{key_node.value}'"
355
+ else
356
+ new_parents = " under #{label} '#{key_node.value}'"
357
+ end
358
+ validate_yaml(key_node, filename, depth, new_parents) # unnecessary?
359
+ validate_yaml(val_node, filename, depth, new_parents)
360
+ end
361
+ end
362
+ private :validate_yaml
363
+
364
+ # Read in yaml file.
365
+ def load_yaml(yaml_file)
366
+ raise "File #{yaml_file} doesn't exist." unless File.exist?(yaml_file)
367
+ # Parse YAML file into a tree of nodes
368
+ # Psych::SyntaxError doesn't inherit from StandardError in some versions,
369
+ # so we want to explicitly catch it if using Psych.
370
+ if defined?(::Psych::SyntaxError)
371
+ rescue_errors = [::StandardError, ::Psych::SyntaxError]
372
+ else
373
+ rescue_errors = [::StandardError]
374
+ end
375
+ yaml_parsed = File.open(yaml_file, 'r') do |f|
376
+ begin
377
+ YAML.parse(f)
378
+ rescue *rescue_errors => e
379
+ raise "unable to parse #{yaml_file}: #{e}"
380
+ end
381
+ end
382
+ if yaml_parsed
383
+ # Validate the node tree
384
+ validate_yaml(yaml_parsed, yaml_file)
385
+ # If validation passed, convert the node tree to a Ruby Hash.
386
+ return yaml_parsed.transform
387
+ else
388
+ # if yaml_file is empty, YAML.parse() returns false.
389
+ # Change this to an empty hash.
390
+ return {}
391
+ end
392
+ end
393
+
394
+ # Check that all resources were pulled in correctly.
395
+ def valid?
396
+ complete_status = true
397
+ @hash.each { |feature, names|
398
+ names.each { |name, ref|
399
+ status = ref.valid?
400
+ debug "Reference does not contain all supported values:\n#{ref}" unless status
401
+ complete_status = (status and complete_status)
402
+ }
403
+ }
404
+ complete_status
405
+ end
406
+
407
+ def to_s
408
+ @hash.each { |feature, names|
409
+ names.each { |name, ref|
410
+ ref.to_s
411
+ }
412
+ }
413
+ end
414
+ end
415
+ end