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,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