diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..d8798cc --- /dev/null +++ b/.pylintrc @@ -0,0 +1,536 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + locally-enabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[BASIC] + +# Naming style matching correct argument names +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style +#argument-rgx= + +# Naming style matching correct attribute names +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style +#class-attribute-rgx= + +# Naming style matching correct class names +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming-style +#class-rgx= + +# Naming style matching correct constant names +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming style matching correct inline iteration names +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style +#inlinevar-rgx= + +# Naming style matching correct method names +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style +#method-rgx= + +# Naming style matching correct module names +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=80 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/README.md b/README.md index e163416..d2896c9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ granularity, it rapidly discovers the vast majority of OSPF problems. * [FAQ](#faq) ## Supported platforms -Today, Cisco IOS/IOS-XE and IOS-XR are supported. The valid `device_type` +Today, Cisco IOS/IOS-XE, IOS-XR, and NX-OS are supported. Valid `device_type` options used for inventory groups are enumerated below. Each platform has a folder in the `devices/` directory, such as `devices/ios/`. The file named `main.yml` is the task list that is included from the main @@ -24,10 +24,12 @@ playbook which begins the device-specific tasks. * `ios`: Cisco classic IOS and Cisco IOS-XE devices. * `iosxr`: Cisco IOS-XR devices. + * `nxos`: Cisco NX-OS devices. Testing was conducted on the following platforms and versions: * Cisco CSR1000v, version 16.07.01a, running in AWS * Cisco XRv9000, version 6.3.1, running in AWS + * Cisco 3172T, version 6.0.2.U6.4a, hardware appliance Control machine information: ``` @@ -235,14 +237,14 @@ Neighbor ID Pri State Dead Time Address Interface ``` ## FAQ -__Q__: A lot of the code between IOS and IOS-XR is the same. Why not combine it? -__A__: The goal is to support more platforms in the future, such as Nexus OS, +__Q__: Most code between IOS, IOS-XR, and NX-OS is the same. Why not combine it? +__A__: The goal is to support more platforms in the future such as Cisco ASA-OS, and possibly non-Cisco devices. These devices will likely return different sets of information. This tool is designed to be __simple__, -not particularly advanced. +not particularly advanced through layered abstractions. __Q__: Why not use an API like RESTCONF or NETCONF instead of SSH + CLI? -__A__: This tool is designed for risk-averse users or networks that are not +__A__: This tool is designed for risk-averse users or managers that are not rapidly migrating to API-based management. It is not an infrastructure-as-code solution and does not manage device configurations. All of the commands used in the playbook can be issued at privilege level 1 to further reduce risk. @@ -257,3 +259,9 @@ level parameters for verification. Furthermore, the detailed statistics checking will alert the user to many errors (authentication, MTU mismatch, etc) at a more general level. The user can check the logs to see the exact commands, which includes the non-parsed interface text. + +__Q__: For NX-OS why didn't you use the `| json` filter from the CLI? +__A__: While this would have saved a lot of parsing code, I did not want to +have an inconsistent overall strategy for one network device. Additionally, +the filter does not render milliseconds properly (eg, SPF throttle timers) +which reduced my confidence in its overall accuracy. diff --git a/group_vars/ospf_routers.yml b/group_vars/ospf_routers.yml index c38578e..4ae7a05 100644 --- a/group_vars/ospf_routers.yml +++ b/group_vars/ospf_routers.yml @@ -1,11 +1,11 @@ --- all_areas: - #area0: - #type: standard # valid values: standard, stub, nssa - #routers: 3 - #drs: 1 - #has_frr: true - #max_lsa3: 333 + # area0: + # type: standard # valid values: standard, stub, nssa + # routers: 3 + # drs: 1 + # has_frr: true + # max_lsa3: 333 area0: type: standard routers: 2 diff --git a/hosts.yml b/hosts.yml index 2984366..3a54276 100644 --- a/hosts.yml +++ b/hosts.yml @@ -1,5 +1,4 @@ --- -# group_vars/ospf_routers.yml all: hosts: localhost: diff --git a/plugins/filter/filter.py b/plugins/filter/filter.py index 04a7c61..9546bbc 100644 --- a/plugins/filter/filter.py +++ b/plugins/filter/filter.py @@ -40,6 +40,13 @@ def filters(): @staticmethod def _read_match(match, key_filler_list=None): + ''' + Helper function which consumes a match object and an optional + list of keys to populate with None values if match is invalid. + Many operations follow this basic workflow, which iterates over + the items captured in the match, attempts to make them integers + whenever possible, and returns the resulting dict. + ''' return_dict = None if match: return_dict = match.groupdict() @@ -54,12 +61,16 @@ def _read_match(match, key_filler_list=None): @staticmethod def _get_match_items(pattern, text, extra_flags=0): + ''' + Helper function that can perform iterative block matching + given a pattern and input text. Additional regex flags (re.DOTALL, etc) + can be optionally specified. Any fields that can be parsed as + integers are converted and the list of dictionaries containing the + matches of each block is returned. + ''' regex = re.compile(pattern, re.VERBOSE + extra_flags) items = [match.groupdict() for match in regex.finditer(text)] for item in items: - # If there is an 'intf' key, make it lowercase - #if 'intf' in item: - # item['intf'] = item['intf'].lower() for key in item.keys(): item[key] = FilterModule._try_int(item[key]) @@ -68,7 +79,7 @@ def _get_match_items(pattern, text, extra_flags=0): @staticmethod def nxos_ospf_traffic(text): ''' - Parses information from the Cisco IOS-XR "show ip ospf traffic" command + Parses information from the Cisco NXOS "show ip ospf traffic" command family. This is useful for verifying various characteristics of an OSPF process/area statistics for troubleshooting. ''' @@ -112,11 +123,10 @@ def nxos_ospf_traffic(text): @staticmethod def nxos_ospf_dbsum(text): ''' - Parses information from the Cisco IOS + Parses information from the Cisco NXOS "show ip ospf database database-summary" command family. This is useful for verifying various characteristics of an OSPF database to count LSAs for simple verification. - Note that this parser is generic enough to cover Cisco IOS-XR also. ''' return_dict = {} process_pattern = r""" @@ -156,6 +166,7 @@ def nxos_ospf_dbsum(text): return_dict.update({'areas': areas}) return return_dict + @staticmethod def nxos_ospf_neighbor(text): ''' @@ -172,25 +183,7 @@ def nxos_ospf_neighbor(text): (?P\d+\.\d+\.\d+\.\d+)\s+ (?P[0-9A-Za-z./-]+) """ - regex = re.compile(pattern, re.VERBOSE) - ospf_neighbors = [] - for s in text.split('\n'): - m = regex.search(s) - if m: - d = m.groupdict() - d['priority'] = FilterModule._try_int(d['priority']) - d['state'] = d['state'].lower() - d['role'] = d['role'].lower() - d['intf'] = d['intf'].lower() - - up_times = d['uptime'].split(':') - times = [FilterModule._try_int(t) for t in up_times] - upsec = times[0] * 3600 + times[1] * 60 + times[2] - d.update({'upsec': upsec}) - - ospf_neighbors.append(d) - - return ospf_neighbors + return FilterModule._ospf_neighbor(pattern, text, ['uptime']) @staticmethod def nxos_ospf_basic(text): @@ -237,7 +230,8 @@ def nxos_ospf_basic(text): areas = [match.groupdict() for match in regex.finditer(text)] for area in areas: area['num_intfs'] = FilterModule._try_int(area['num_intfs']) - area['id'] = FilterModule._try_int(ipaddress.IPv4Address(area['id_dd'])) + converted_dd = ipaddress.IPv4Address(area['id_dd']) + area['id'] = FilterModule._try_int(converted_dd) if not area['type']: area['type'] = 'standard' else: @@ -275,25 +269,7 @@ def ios_ospf_neighbor(text): (?P\d+\.\d+\.\d+\.\d+)\s+ (?P[0-9A-Za-z./-]+) """ - regex = re.compile(pattern, re.VERBOSE) - ospf_neighbors = [] - for s in text.split('\n'): - m = regex.search(s) - if m: - d = m.groupdict() - d['priority'] = FilterModule._try_int(d['priority']) - d['state'] = d['state'].lower() - d['role'] = d['role'].lower() - d['intf'] = d['intf'].lower() - - dead_times = d['deadtime'].split(':') - times = [FilterModule._try_int(t) for t in dead_times] - deadsec = times[0] * 3600 + times[1] * 60 + times[2] - d.update({'deadsec': deadsec}) - - ospf_neighbors.append(d) - - return ospf_neighbors + return FilterModule._ospf_neighbor(pattern, text, ['deadtime']) @staticmethod def ios_ospf_basic(text): @@ -456,18 +432,18 @@ def ios_ospf_frr(text): """ regex = re.compile(pattern, re.VERBOSE) frr_area_dict = {} - for s in text.split('\n'): - m = regex.search(s) - if m: - d = m.groupdict() - area = 'area' + d['id'] - d['id'] = FilterModule._try_int(d['id']) - d['rlfa'] = d['rlfa'].lower() == 'yes' - d['tilfa'] = d['tilfa'].lower() == 'yes' - d['pref_pri'] = d['pref_pri'].lower() - d['topology'] = d['topology'].lower() - - frr_area_dict.update({area: d}) + for line in text.split('\n'): + match = regex.search(line) + if match: + gdict = match.groupdict() + area = 'area' + gdict['id'] + gdict['id'] = FilterModule._try_int(gdict['id']) + gdict['rlfa'] = gdict['rlfa'].lower() == 'yes' + gdict['tilfa'] = gdict['tilfa'].lower() == 'yes' + gdict['pref_pri'] = gdict['pref_pri'].lower() + gdict['topology'] = gdict['topology'].lower() + + frr_area_dict.update({area: gdict}) return frr_area_dict @@ -489,17 +465,17 @@ def ios_bfd_neighbor(text): """ regex = re.compile(pattern, re.VERBOSE) bfd_neighbors = [] - for s in text.split('\n'): - m = regex.search(s) - if m: - d = m.groupdict() - d['ld'] = FilterModule._try_int(d['ld']) - d['rd'] = FilterModule._try_int(d['rd']) - d['rhrs'] = d['rhrs'].lower() - d['state'] = d['state'].lower() - d['intf'] = d['intf'].lower() - - bfd_neighbors.append(d) + for line in text.split('\n'): + match = regex.search(line) + if match: + gdict = match.groupdict() + gdict['ld'] = FilterModule._try_int(gdict['ld']) + gdict['rd'] = FilterModule._try_int(gdict['rd']) + gdict['rhrs'] = gdict['rhrs'].lower() + gdict['state'] = gdict['state'].lower() + gdict['intf'] = gdict['intf'].lower() + + bfd_neighbors.append(gdict) return bfd_neighbors @@ -518,7 +494,7 @@ def check_bfd_up(bfd_nbr_list, ospf_nbr): is_up = bfd_nbr['state'] == 'up' and bfd_nbr['rhrs'] == 'up' return is_up - raise ValueError('Peer {0} not found in bfd_nbr_list'.format(ospf_nbr['peer'])) + raise ValueError('{0} not in bfd_nbr_list'.format(ospf_nbr['peer'])) @staticmethod def iosxr_ospf_neighbor(text): @@ -537,28 +513,40 @@ def iosxr_ospf_neighbor(text): (?P[0-9:]+)\s+ (?P[0-9A-Za-z./-]+) """ + return FilterModule._ospf_neighbor( + pattern, text, ['deadtime', 'uptime']) + + @staticmethod + def _ospf_neighbor(pattern, text, time_keys=None): + ''' + Helper function specific to OSPF neighbor parsing. Each device type + is slightly different in terms of the information provided, but + most fields are the same. The time_keys parameter is a list of keys + which are expected to have values in the format "hh:mm:ss". These + are commonly uptime, deadtime, etc ... and are most useful when + converted into seconds as an integer for comparative purposes. + ''' regex = re.compile(pattern, re.VERBOSE) ospf_neighbors = [] - for s in text.split('\n'): - m = regex.search(s) - if m: - d = m.groupdict() - d['priority'] = FilterModule._try_int(d['priority']) - d['state'] = d['state'].lower() - d['role'] = d['role'].lower() - d['intf'] = d['intf'].lower() - - dead_times = d['deadtime'].split(':') - times = [FilterModule._try_int(t) for t in dead_times] - deadsec = times[0] * 3600 + times[1] * 60 + times[2] - d.update({'deadsec': deadsec}) - - up_times = d['uptime'].split(':') - times = [FilterModule._try_int(t) for t in up_times] - upsec = times[0] * 3600 + times[1] * 60 + times[2] - d.update({'upsec': upsec}) - - ospf_neighbors.append(d) + for line in text.split('\n'): + match = regex.search(line) + if match: + gdict = match.groupdict() + gdict['priority'] = FilterModule._try_int(gdict['priority']) + gdict['state'] = gdict['state'].lower() + gdict['role'] = gdict['role'].lower() + gdict['intf'] = gdict['intf'].lower() + + # If time keys is specified, iterate over the keys and perform + # the math to convert hh:mm:ss to an integer of summed seconds. + if time_keys: + for k in time_keys: + times = gdict[k].split(':') + parts = [FilterModule._try_int(t) for t in times] + totalsec = parts[0] * 3600 + parts[1] * 60 + parts[2] + gdict.update({k + '_sec': totalsec}) + + ospf_neighbors.append(gdict) return ospf_neighbors @@ -572,11 +560,14 @@ def iosxr_ospf_basic(text): return_dict = {} process_pattern = r""" - Routing\s+Process\s+"ospf\s+(?P\d+)"\s+with\s+ID\s+(?P\d+\.\d+\.\d+\.\d+) + Routing\s+Process\s+"ospf\s+(?P\d+)"\s+with\s+ID\s+ + (?P\d+\.\d+\.\d+\.\d+) .* \s*Initial\s+SPF\s+schedule\s+delay\s+(?P\d+)\s+msecs - \s*Minimum\s+hold\s+time\s+between\s+two\s+consecutive\s+SPFs\s+(?P\d+)\s+msecs - \s*Maximum\s+wait\s+time\s+between\s+two\s+consecutive\s+SPFs\s+(?P\d+)\s+msecs + \s*Minimum\s+hold\s+time\s+between\s+two\s+consecutive + \s+SPFs\s+(?P\d+)\s+msecs + \s*Maximum\s+wait\s+time\s+between\s+two\s+consecutive + \s+SPFs\s+(?P\d+)\s+msecs """ regex = re.compile(process_pattern, re.VERBOSE + re.DOTALL) match = regex.search(text) diff --git a/lint.sh b/tests/lint.sh similarity index 96% rename from lint.sh rename to tests/lint.sh index c18a6d6..3cfd773 100755 --- a/lint.sh +++ b/tests/lint.sh @@ -14,7 +14,7 @@ echo "YAML linting started" for f in $(find . -name "*.yml"); do # Print the filename, then run 'yamllist' in strict mode echo "checking $f" - yamllint -s $f + yamllint --strict $f # Sum the rc from yamllint with the sum rc=$((rc + $?)) done @@ -25,7 +25,7 @@ echo "Python linting started" for f in $(find . -name "*.py"); do # Print the filename, then run 'pylint' echo "checking $f" - pylint $f + pylint --score n $f # Sum the rc from pylint with the sum rc=$((rc + $?)) done diff --git a/tests/test_ios_ospf_neighbor.yml b/tests/test_ios_ospf_neighbor.yml index 4158846..6d2a68b 100644 --- a/tests/test_ios_ospf_neighbor.yml +++ b/tests/test_ios_ospf_neighbor.yml @@ -24,7 +24,7 @@ - "data[0].state == 'full'" - "data[0].role == '-'" - "data[0].deadtime == '00:00:35'" - - "data[0].deadsec == 35" + - "data[0].deadtime_sec == 35" - "data[0].peer == '10.125.95.7'" - "data[0].intf == 'gigabiteth0/1'" @@ -36,7 +36,7 @@ - "data[1].state == 'full'" - "data[1].role == 'dr'" - "data[1].deadtime == '00:00:34'" - - "data[1].deadsec == 34" + - "data[1].deadtime_sec == 34" - "data[1].peer == '10.125.95.137'" - "data[1].intf == 'port-chan8.33'" @@ -48,7 +48,7 @@ - "data[2].state == '2way'" - "data[2].role == 'drother'" - "data[2].deadtime == '11:22:33'" - - "data[2].deadsec == 40953" + - "data[2].deadtime_sec == 40953" - "data[2].peer == '10.108.0.0'" - "data[2].intf == 'nve8'" ... diff --git a/tests/test_iosxr_ospf_neighbor.yml b/tests/test_iosxr_ospf_neighbor.yml index 6f1f58a..662c374 100644 --- a/tests/test_iosxr_ospf_neighbor.yml +++ b/tests/test_iosxr_ospf_neighbor.yml @@ -32,10 +32,10 @@ - "data[0].state == 'full'" - "data[0].role == 'dr'" - "data[0].deadtime == '00:00:32'" - - "data[0].deadsec == 32" + - "data[0].deadtime_sec == 32" - "data[0].peer == '192.168.1.11'" - "data[0].uptime == '00:18:57'" - - "data[0].upsec == 1137" + - "data[0].uptime_sec == 1137" - "data[0].intf | lower == 'gi0/0/0/0.511'" msg: "parsing problem. see JSON dump from previous task" @@ -47,10 +47,10 @@ - "data[1].state == 'full'" - "data[1].role == '-'" - "data[1].deadtime == '00:01:32'" - - "data[1].deadsec == 92" + - "data[1].deadtime_sec == 92" - "data[1].peer == '192.168.1.12'" - "data[1].uptime == '01:18:57'" - - "data[1].upsec == 4737" + - "data[1].uptime_sec == 4737" - "data[1].intf | lower == 'gi1/2/3/4.512'" msg: "parsing problem. see JSON dump from previous task" @@ -62,10 +62,10 @@ - "data[2].state == 'init'" - "data[2].role == '-'" - "data[2].deadtime == '00:02:32'" - - "data[2].deadsec == 152" + - "data[2].deadtime_sec == 152" - "data[2].peer == '192.168.1.13'" - "data[2].uptime == '02:18:57'" - - "data[2].upsec == 8337" + - "data[2].uptime_sec == 8337" - "data[2].intf | lower == 'tunnel-ip13'" msg: "parsing problem. see JSON dump from previous task" ... diff --git a/tests/test_nxos_ospf_neighbor.yml b/tests/test_nxos_ospf_neighbor.yml index 4e30aaf..8e9d44f 100644 --- a/tests/test_nxos_ospf_neighbor.yml +++ b/tests/test_nxos_ospf_neighbor.yml @@ -25,7 +25,7 @@ - "data[0].state == 'full'" - "data[0].role == '-'" - "data[0].uptime == '00:06:47'" - - "data[0].upsec == 407" + - "data[0].uptime_sec == 407" - "data[0].peer == '192.168.0.2'" - "data[0].intf == 'eth1/43'" @@ -37,7 +37,7 @@ - "data[1].state == '2way'" - "data[1].role == 'bdr'" - "data[1].uptime == '21:01:05'" - - "data[1].upsec == 75665" + - "data[1].uptime_sec == 75665" - "data[1].peer == '10.192.17.6'" - "data[1].intf == 'port-cha1'" ... diff --git a/test_playbook.yml b/tests/unittest_playbook.yml similarity index 94% rename from test_playbook.yml rename to tests/unittest_playbook.yml index 5cd4485..0d12aa7 100644 --- a/test_playbook.yml +++ b/tests/unittest_playbook.yml @@ -3,6 +3,8 @@ # for the introduction of any regressions during development. - name: "Perform automated filter (unit) testing" hosts: localhost + environment: + ANSIBLE_CONFIG: "../ansible.cfg" tasks: # All test files, which are lists of ansible tasks, begin with the @@ -18,7 +20,7 @@ # This is uncommonly used and should be avoided for general purpose tests. - name: "SYS >> Get files matching {{ TASKS | quote }}" find: - path: "{{ playbook_dir }}/tests" + path: "{{ playbook_dir }}" patterns: "{{ TASKS }}" register: FIND_LIST